diff --git a/.github/workflows/auto_release.yml b/.github/workflows/auto_release.yml index 146037529..e6f84e716 100644 --- a/.github/workflows/auto_release.yml +++ b/.github/workflows/auto_release.yml @@ -23,6 +23,33 @@ jobs: echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV echo ${{ github.ref_name }} > dist/assets/version zip -r dist.zip dist + + - name: Upload to Cloudflare R2 + env: + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + R2_BUCKET_NAME: "astrbot" + R2_OBJECT_NAME: "astrbot-webui-latest.zip" + VERSION_TAG: ${{ github.ref_name }} + run: | + echo "Installing rclone..." + curl https://rclone.org/install.sh | sudo bash + + echo "Configuring rclone remote..." + mkdir -p ~/.config/rclone + cat < ~/.config/rclone/rclone.conf + [r2] + type = s3 + provider = Cloudflare + access_key_id = $R2_ACCESS_KEY_ID + secret_access_key = $R2_SECRET_ACCESS_KEY + endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com + EOF + + echo "Uploading dist.zip to R2 bucket: $R2_BUCKET_NAME/$R2_OBJECT_NAME" + rclone copy dashboard/dist.zip r2:$R2_BUCKET_NAME/$R2_OBJECT_NAME --progress + rclone copy dashboard/dist.zip r2:$R2_BUCKET_NAME/astrbot-webui-${VERSION_TAG}.zip --progress - name: Fetch Changelog run: | diff --git a/README.md b/README.md index 2bc168358..5846ebebc 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,15 @@ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用 ## ✨ 近期更新 -1. AstrBot 现已支持接入 [MCP](https://modelcontextprotocol.io/) 服务器! +
1. AstrBot 现已自带知识库能力 + + 📚 详见[文档](https://astrbot.app/use/knowledge-base.html) + + ![image](https://github.com/user-attachments/assets/28b639b0-bb5c-4958-8e94-92ae8cfd1ab4) + +
+ +2. AstrBot 现已支持接入 [MCP](https://modelcontextprotocol.io/) 服务器! ## ✨ 主要功能 @@ -171,7 +179,6 @@ pre-commit install - Star 这个项目! - 在[爱发电](https://afdian.com/a/soulter)支持我! -- 在[微信](https://drive.soulter.top/f/pYfA/d903f4fa49a496fda3f16d2be9e023b5.png)支持我~ ## ✨ Demo diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 708b0876d..6af27337b 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -5,7 +5,7 @@ import os from astrbot.core.utils.astrbot_path import get_astrbot_data_path -VERSION = "3.5.12" +VERSION = "3.5.13" DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db") # 默认配置 @@ -862,8 +862,48 @@ CONFIG_METADATA_2 = { "api_base": "https://openspeech.bytedance.com/api/v1/tts", "timeout": 20, }, + "OpenAI Embedding": { + "id": "openai_embedding", + "type": "openai_embedding", + "provider_type": "embedding", + "enable": True, + "embedding_api_key": "", + "embedding_api_base": "", + "embedding_model": "", + "embedding_dimensions": 1536, + "timeout": 20, + }, + "Gemini Embedding": { + "id": "gemini_embedding", + "type": "gemini_embedding", + "provider_type": "embedding", + "enable": True, + "embedding_api_key": "", + "embedding_api_base": "", + "embedding_model": "gemini-embedding-exp-03-07", + "embedding_dimensions": 768, + "timeout": 20, + }, }, "items": { + "embedding_dimensions": { + "description": "嵌入维度", + "type": "int", + "hint": "嵌入向量的维度。根据模型不同,可能需要调整,请参考具体模型的文档。此配置项请务必填写正确,否则将导致向量数据库无法正常工作。", + }, + "embedding_model": { + "description": "嵌入模型", + "type": "string", + "hint": "嵌入模型名称。", + }, + "embedding_api_key": { + "description": "API Key", + "type": "string", + }, + "embedding_api_base": { + "description": "API Base URL", + "type": "string", + }, "volcengine_cluster": { "type": "string", "description": "火山引擎集群", diff --git a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py index 262a459e3..18ee3189c 100644 --- a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +++ b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py @@ -29,9 +29,9 @@ class EmbeddingStorage: Raises: ValueError: 如果向量的维度与存储的维度不匹配 """ - if vector.shape[0] != self.dimention: + if vector.shape[0] != self.dimension: raise ValueError( - f"向量维度不匹配, 期望: {self.dimention}, 实际: {vector.shape[0]}" + f"向量维度不匹配, 期望: {self.dimension}, 实际: {vector.shape[0]}" ) self.index.add_with_ids(vector.reshape(1, -1), np.array([id])) self.storage[id] = vector diff --git a/astrbot/core/db/vec_db/faiss_impl/vec_db.py b/astrbot/core/db/vec_db/faiss_impl/vec_db.py index e4122e547..8d95c2501 100644 --- a/astrbot/core/db/vec_db/faiss_impl/vec_db.py +++ b/astrbot/core/db/vec_db/faiss_impl/vec_db.py @@ -30,19 +30,13 @@ class FaissVecDB(BaseVecDB): async def initialize(self): await self.document_storage.initialize() - async def insert( - self, - content: str, - metadata: dict = None, - id: str = None, - ) -> int: + async def insert(self, content: str, metadata: dict = None, id: str = None) -> int: """ 插入一条文本和其对应向量,自动生成 ID 并保持一致性。 """ metadata = metadata or {} str_id = id or str(uuid.uuid4()) # 使用 UUID 作为原始 ID - # 获取向量 vector = await self.embedding_provider.get_embedding(content) vector = np.array(vector, dtype=np.float32) async with self.document_storage.connection.cursor() as cursor: @@ -54,9 +48,9 @@ class FaissVecDB(BaseVecDB): result = await self.document_storage.get_document_by_doc_id(str_id) int_id = result["id"] - # 插入向量到 FAISS - await self.embedding_storage.insert(vector, int_id) - return int_id + # 插入向量到 FAISS + await self.embedding_storage.insert(vector, int_id) + return int_id async def retrieve( self, query: str, k: int = 5, fetch_k: int = 20, metadata_filters: dict = None diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index 6ad67da55..e01e46cf9 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -19,6 +19,7 @@ class ProviderType(enum.Enum): CHAT_COMPLETION = "chat_completion" SPEECH_TO_TEXT = "speech_to_text" TEXT_TO_SPEECH = "text_to_speech" + EMBEDDING = "embedding" @dataclass @@ -155,7 +156,9 @@ class ProviderRequest: if self.image_urls: user_content = { "role": "user", - "content": [{"type": "text", "text": self.prompt if self.prompt else "[图片]"}], + "content": [ + {"type": "text", "text": self.prompt if self.prompt else "[图片]"} + ], } for image_url in self.image_urls: if image_url.startswith("http"): diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 78337ce95..edfd9f581 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -98,6 +98,8 @@ class ProviderManager: """加载的 Speech To Text Provider 的实例""" self.tts_provider_insts: List[TTSProvider] = [] """加载的 Text To Speech Provider 的实例""" + self.embedding_provider_insts: List[Provider] = [] + """加载的 Embedding Provider 的实例""" self.inst_map = {} """Provider 实例映射. key: provider_id, value: Provider 实例""" self.llm_tools = llm_tools @@ -211,6 +213,10 @@ class ProviderManager: from .sources.volcengine_tts import ( ProviderVolcengineTTS as ProviderVolcengineTTS, ) + case "openai_embedding": + from .sources.openai_embedding_source import ( + OpenAIEmbeddingProvider as OpenAIEmbeddingProvider, + ) except (ImportError, ModuleNotFoundError) as e: logger.critical( f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。" @@ -290,6 +296,14 @@ class ProviderManager: if not self.curr_provider_inst: self.curr_provider_inst = inst + elif provider_metadata.provider_type == ProviderType.EMBEDDING: + inst = provider_metadata.cls_type( + provider_config, self.provider_settings + ) + if getattr(inst, "initialize", None): + await inst.initialize() + self.embedding_provider_insts.append(inst) + self.inst_map[provider_config["id"]] = inst except Exception as e: logger.error(traceback.format_exc()) diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py index 7019113c7..c285ebd42 100644 --- a/astrbot/core/provider/provider.py +++ b/astrbot/core/provider/provider.py @@ -192,6 +192,11 @@ class EmbeddingProvider(AbstractProvider): """获取文本的向量""" ... + @abc.abstractmethod + async def get_embeddings(self, text: list[str]) -> list[list[float]]: + """批量获取文本的向量""" + ... + @abc.abstractmethod def get_dim(self) -> int: """获取向量的维度""" diff --git a/astrbot/core/provider/sources/gemini_embedding_source.py b/astrbot/core/provider/sources/gemini_embedding_source.py new file mode 100644 index 000000000..baccf52a2 --- /dev/null +++ b/astrbot/core/provider/sources/gemini_embedding_source.py @@ -0,0 +1,63 @@ +from google import genai +from google.genai import types +from google.genai.errors import APIError +from ..provider import EmbeddingProvider +from ..register import register_provider_adapter +from ..entities import ProviderType + + +@register_provider_adapter( + "gemini_embedding", + "Google Gemini Embedding 提供商适配器", + provider_type=ProviderType.EMBEDDING, +) +class GeminiEmbeddingProvider(EmbeddingProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config, provider_settings) + self.provider_config = provider_config + self.provider_settings = provider_settings + + api_key: str = provider_config.get("embedding_api_key") + api_base: str = provider_config.get("embedding_api_base", None) + timeout: int = int(provider_config.get("timeout", 20)) + + http_options = types.HttpOptions(timeout=timeout * 1000) + if api_base: + if api_base.endswith("/"): + api_base = api_base[:-1] + http_options.base_url = api_base + + self.client = genai.Client(api_key=api_key, http_options=http_options).aio + + self.model = provider_config.get( + "embedding_model", "gemini-embedding-exp-03-07" + ) + self.dimension = provider_config.get("embedding_dimensions", 768) + + async def get_embedding(self, text: str) -> list[float]: + """ + 获取文本的嵌入 + """ + try: + result = await self.client.models.embed_content( + model=self.model, contents=text + ) + return result.embeddings[0].values + except APIError as e: + raise Exception(f"Gemini Embedding API请求失败: {e.message}") + + async def get_embeddings(self, texts: list[str]) -> list[list[float]]: + """ + 批量获取文本的嵌入 + """ + try: + result = await self.client.models.embed_content( + model=self.model, contents=texts + ) + return [embedding.values for embedding in result.embeddings] + except APIError as e: + raise Exception(f"Gemini Embedding API批量请求失败: {e.message}") + + def get_dim(self) -> int: + """获取向量的维度""" + return self.dimension diff --git a/astrbot/core/provider/sources/openai_embedding_source.py b/astrbot/core/provider/sources/openai_embedding_source.py new file mode 100644 index 000000000..f43152473 --- /dev/null +++ b/astrbot/core/provider/sources/openai_embedding_source.py @@ -0,0 +1,43 @@ +from openai import AsyncOpenAI +from ..provider import EmbeddingProvider +from ..register import register_provider_adapter +from ..entities import ProviderType + + +@register_provider_adapter( + "openai_embedding", + "OpenAI API Embedding 提供商适配器", + provider_type=ProviderType.EMBEDDING, +) +class OpenAIEmbeddingProvider(EmbeddingProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config, provider_settings) + self.provider_config = provider_config + self.provider_settings = provider_settings + self.client = AsyncOpenAI( + api_key=provider_config.get("embedding_api_key"), + base_url=provider_config.get( + "embedding_api_base", "https://api.openai.com/v1" + ), + timeout=int(provider_config.get("timeout", 20)), + ) + self.model = provider_config.get("embedding_model", "text-embedding-3-small") + self.dimension = provider_config.get("embedding_dimensions", 1536) + + async def get_embedding(self, text: str) -> list[float]: + """ + 获取文本的嵌入 + """ + embedding = await self.client.embeddings.create(input=text, model=self.model) + return embedding.data[0].embedding + + async def get_embeddings(self, texts: list[str]) -> list[list[float]]: + """ + 批量获取文本的嵌入 + """ + embeddings = await self.client.embeddings.create(input=texts, model=self.model) + return [item.embedding for item in embeddings.data] + + def get_dim(self) -> int: + """获取向量的维度""" + return self.dimension diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index 7cb3ffd1c..880b0c72c 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -125,11 +125,8 @@ class Context: self.provider_manager.provider_insts.append(provider) def get_provider_by_id(self, provider_id: str) -> Provider: - """通过 ID 获取用于文本生成任务的 LLM Provider(Chat_Completion 类型)。""" - for provider in self.provider_manager.provider_insts: - if provider.meta().id == provider_id: - return provider - return None + """通过 ID 获取对应的 LLM Provider(Chat_Completion 类型)。""" + return self.provider_manager.inst_map.get(provider_id) def get_all_providers(self) -> List[Provider]: """获取所有用于文本生成任务的 LLM Provider(Chat_Completion 类型)。""" @@ -301,5 +298,11 @@ class Context: """ self._register_tasks.append(task) - def register_web_api(self, route: str, view_handler: Awaitable, methods: list, desc: str): + def register_web_api( + self, route: str, view_handler: Awaitable, methods: list, desc: str + ): + for idx, api in enumerate(self.registered_web_apis): + if api[0] == route and methods == api[2]: + self.registered_web_apis[idx] = (route, view_handler, methods, desc) + return self.registered_web_apis.append((route, view_handler, methods, desc)) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index ff07de712..44e018dfc 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -166,7 +166,7 @@ class PluginManager: plugins.extend(_p) return plugins - def _check_plugin_dept_update(self, target_plugin: str = None): + async def _check_plugin_dept_update(self, target_plugin: str = None): """检查插件的依赖 如果 target_plugin 为 None,则检查所有插件的依赖 """ @@ -185,7 +185,7 @@ class PluginManager: pth = os.path.join(plugin_path, "requirements.txt") logger.info(f"正在安装插件 {p} 所需的依赖库: {pth}") try: - pip_installer.install(requirements_path=pth) + await pip_installer.install(requirements_path=pth) except Exception as e: logger.error(f"更新插件 {p} 的依赖失败。Code: {str(e)}") @@ -407,7 +407,7 @@ class PluginManager: module = __import__(path, fromlist=[module_str]) except (ModuleNotFoundError, ImportError): # 尝试安装依赖 - self._check_plugin_dept_update(target_plugin=root_dir_name) + await self._check_plugin_dept_update(target_plugin=root_dir_name) module = __import__(path, fromlist=[module_str]) except Exception as e: logger.error(traceback.format_exc()) diff --git a/astrbot/core/utils/pip_installer.py b/astrbot/core/utils/pip_installer.py index 0163b11b4..a7c04d3d9 100644 --- a/astrbot/core/utils/pip_installer.py +++ b/astrbot/core/utils/pip_installer.py @@ -1,5 +1,5 @@ import logging -from pip import main as pip_main +import asyncio logger = logging.getLogger("astrbot") @@ -9,7 +9,7 @@ class PipInstaller: self.pip_install_arg = pip_install_arg self.pypi_index_url = pypi_index_url - def install( + async def install( self, package_name: str = None, requirements_path: str = None, @@ -29,12 +29,29 @@ class PipInstaller: args.extend(self.pip_install_arg.split()) logger.info(f"Pip 包管理器: pip {' '.join(args)}") + try: + process = await asyncio.create_subprocess_exec( + "pip", *args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) - result_code = pip_main(args) + assert process.stdout is not None + async for line in process.stdout: + logger.info(line.decode().strip()) - # 清除 pip.main 导致的多余的 logging handlers - for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) + await process.wait() - if result_code != 0: - raise Exception(f"安装失败,错误码:{result_code}") + if process.returncode != 0: + raise Exception(f"安装失败,错误码:{process.returncode}") + except FileNotFoundError: + # 没有 pip + from pip import main as pip_main + result_code = await asyncio.to_thread(pip_main, args) + + # 清除 pip.main 导致的多余的 logging handlers + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + + if result_code != 0: + raise Exception(f"安装失败,错误码:{result_code}") diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index b2677de10..4b25c977f 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -9,6 +9,7 @@ from astrbot.core.platform.register import platform_registry from astrbot.core.provider.register import provider_registry from astrbot.core.star.star import star_registry from astrbot.core import logger +import asyncio def try_cast(value: str, type_: str): @@ -164,9 +165,84 @@ class ConfigRoute(Route): "/config/provider/update": ("POST", self.post_update_provider), "/config/provider/delete": ("POST", self.post_delete_provider), "/config/llmtools": ("GET", self.get_llm_tools), + "/config/provider/check_status": ("GET", self.check_all_providers_status), + "/config/provider/list": ("GET", self.get_provider_config_list), } self.register_routes() + async def _test_single_provider(self, provider): + """辅助函数:测试单个 provider 的可用性""" + meta = provider.meta() + provider_name = provider.provider_config.get("id", "Unknown Provider") + if not provider_name and meta: + provider_name = meta.id + elif not provider_name: + provider_name = "Unknown Provider" + status_info = { + "id": meta.id if meta else "Unknown ID", + "model": meta.model if meta else "Unknown Model", + "type": meta.type if meta else "Unknown Type", + "name": provider_name, + "status": "unavailable", # 默认为不可用 + "error": None, + } + logger.debug(f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})") + try: + logger.debug(f"Sending 'Ping' to provider: {status_info['name']}") + response = await asyncio.wait_for(provider.text_chat(prompt="Ping"), timeout=20.0) # 超时 20 秒 + logger.debug(f"Received response from {status_info['name']}: {response}") + # 只要 text_chat 调用成功返回一个 LLMResponse 对象 (即 response 不为 None),就认为可用 + if response is not None: + status_info["status"] = "available" + response_text_snippet = "" + if hasattr(response, 'completion_text') and response.completion_text: + response_text_snippet = response.completion_text[:70] + "..." if len(response.completion_text) > 70 else response.completion_text + elif hasattr(response, 'result_chain') and response.result_chain: + try: + response_text_snippet = response.result_chain.get_plain_text()[:70] + "..." if len(response.result_chain.get_plain_text()) > 70 else response.result_chain.get_plain_text() + except: + pass + logger.info(f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'") + else: + # 这个分支理论上不应该被走到,除非 text_chat 实现可能返回 None + status_info["error"] = "Test call returned None, but expected an LLMResponse object." + logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.") + + except asyncio.TimeoutError: + status_info["error"] = "Connection timed out after 10 seconds during test call." + logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.") + except Exception as e: + error_message = str(e) + status_info["error"] = error_message + logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}") + logger.debug(f"Traceback for {status_info['name']}:\n{traceback.format_exc()}") + return status_info + + async def check_all_providers_status(self): + """ + API 接口: 检查所有 LLM Providers 的状态 + """ + logger.info("API call received: /config/provider/check_status") + try: + all_providers: typing.List = self.core_lifecycle.star_context.get_all_providers() + logger.debug(f"Found {len(all_providers)} providers to check.") + + if not all_providers: + logger.info("No providers found to check.") + return Response().ok([]).__dict__ + + tasks = [self._test_single_provider(p) for p in all_providers] + logger.debug(f"Created {len(tasks)} tasks for concurrent provider checks.") + + results = await asyncio.gather(*tasks) + logger.info(f"Provider status check completed. Results: {results}") + + return Response().ok(results).__dict__ + except Exception as e: + logger.error(f"Critical error in check_all_providers_status: {str(e)}") + logger.error(traceback.format_exc()) + return Response().error(f"检查 Provider 状态时发生严重错误: {str(e)}").__dict__ + async def get_configs(self): # plugin_name 为空时返回 AstrBot 配置 # 否则返回指定 plugin_name 的插件配置 @@ -175,6 +251,17 @@ class ConfigRoute(Route): return Response().ok(await self._get_astrbot_config()).__dict__ return Response().ok(await self._get_plugin_config(plugin_name)).__dict__ + async def get_provider_config_list(self): + provider_type = request.args.get("provider_type", None) + if not provider_type: + return Response().error("缺少参数 provider_type").__dict__ + provider_list = [] + astrbot_config = self.core_lifecycle.astrbot_config + for provider in astrbot_config["provider"]: + if provider.get("provider_type", None) == provider_type: + provider_list.append(provider) + return Response().ok(provider_list).__dict__ + async def post_astrbot_configs(self): post_configs = await request.json try: diff --git a/astrbot/dashboard/routes/log.py b/astrbot/dashboard/routes/log.py index f99110530..a8cf34c95 100644 --- a/astrbot/dashboard/routes/log.py +++ b/astrbot/dashboard/routes/log.py @@ -23,6 +23,7 @@ class LogRoute(Route): **message, # see astrbot/core/log.py } yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" + await asyncio.sleep(0.07) # 控制发送频率,避免过快 except asyncio.CancelledError: pass except BaseException as e: diff --git a/astrbot/dashboard/routes/static_file.py b/astrbot/dashboard/routes/static_file.py index 36a582bee..3d3d0ca51 100644 --- a/astrbot/dashboard/routes/static_file.py +++ b/astrbot/dashboard/routes/static_file.py @@ -13,6 +13,9 @@ class StaticFileRoute(Route): "/extension", "/dashboard/default", "/alkaid", + "/alkaid/knowledge-base", + "/alkaid/long-term-memory", + "/alkaid/other", "/console", "/chat", "/settings", diff --git a/astrbot/dashboard/routes/update.py b/astrbot/dashboard/routes/update.py index 44adf2591..f88e9a208 100644 --- a/astrbot/dashboard/routes/update.py +++ b/astrbot/dashboard/routes/update.py @@ -91,7 +91,7 @@ class UpdateRoute(Route): # pip 更新依赖 logger.info("更新依赖中...") try: - pip_installer.install(requirements_path="requirements.txt") + await pip_installer.install(requirements_path="requirements.txt") except Exception as e: logger.error(f"更新依赖失败: {e}") @@ -140,7 +140,7 @@ class UpdateRoute(Route): if not package: return Response().error("缺少参数 package 或不合法。").__dict__ try: - pip_installer.install(package, mirror=mirror) + await pip_installer.install(package, mirror=mirror) return Response().ok(None, "安装成功。").__dict__ except Exception as e: logger.error(f"/api/update_pip: {traceback.format_exc()}") diff --git a/changelogs/v3.5.13.md b/changelogs/v3.5.13.md new file mode 100644 index 000000000..7ff838da4 --- /dev/null +++ b/changelogs/v3.5.13.md @@ -0,0 +1,9 @@ +# What's Changed + +1. 新增:WebUI 支持暗夜模式。 +2. 修复:修复 WebUI Chat 接口的未授权访问安全漏洞、插件 README 可能存在的 XSS 注入漏洞。 +3. 优化:优化 Vec DB 在 indexing 过程时的数据库事务处理。 +4. 修复:WebUI 下,插件市场的推荐卡片无法点击帮助文档的问题。 +5. 新增:知识库。 +6. 新增:WebUI 提供商测试功能,一键检测可用性。 +7. 新增:WebUI 提供商分类功能,按能力分类提供商。 diff --git a/dashboard/src/components/shared/ConsoleDisplayer.vue b/dashboard/src/components/shared/ConsoleDisplayer.vue index e780d6569..a05c6eea7 100644 --- a/dashboard/src/components/shared/ConsoleDisplayer.vue +++ b/dashboard/src/components/shared/ConsoleDisplayer.vue @@ -5,7 +5,7 @@ import { useCommonStore } from '@/stores/common';