Merge remote-tracking branch 'origin/master' into feat/astrbot-agent-mode
This commit is contained in:
+3
-1
@@ -30,4 +30,6 @@ packages/python_interpreter/workplace
|
||||
.conda/
|
||||
.idea
|
||||
pytest.ini
|
||||
.astrbot
|
||||
.astrbot
|
||||
|
||||
uv.lock
|
||||
+8
-13
@@ -4,8 +4,6 @@ WORKDIR /AstrBot
|
||||
COPY . /AstrBot/
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
npm \
|
||||
gcc \
|
||||
build-essential \
|
||||
python3-dev \
|
||||
@@ -13,23 +11,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libssl-dev \
|
||||
ca-certificates \
|
||||
bash \
|
||||
ffmpeg \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt-get update && apt-get install -y curl gnupg && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python -m pip install uv
|
||||
RUN uv pip install -r requirements.txt --no-cache-dir --system
|
||||
RUN uv pip install socksio uv pyffmpeg pilk --no-cache-dir --system
|
||||
RUN uv pip install socksio uv pilk --no-cache-dir --system
|
||||
|
||||
# 释出 ffmpeg
|
||||
RUN python -c "from pyffmpeg import FFmpeg; ff = FFmpeg();"
|
||||
|
||||
# add /root/.pyffmpeg/bin/ffmpeg to PATH, inorder to use ffmpeg
|
||||
RUN echo 'export PATH=$PATH:/root/.pyffmpeg/bin' >> ~/.bashrc
|
||||
|
||||
EXPOSE 6185
|
||||
EXPOSE 6185
|
||||
EXPOSE 6186
|
||||
|
||||
CMD [ "python", "main.py" ]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.2.1"
|
||||
VERSION = "4.3.1"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -64,7 +64,7 @@ DEFAULT_CONFIG = {
|
||||
"datetime_system_prompt": True,
|
||||
"default_personality": "default",
|
||||
"persona_pool": ["*"],
|
||||
"prompt_prefix": "",
|
||||
"prompt_prefix": "{{prompt}}",
|
||||
"max_context_length": -1,
|
||||
"dequeue_context_length": 1,
|
||||
"streaming_response": False,
|
||||
@@ -2118,10 +2118,12 @@ CONFIG_METADATA_3 = {
|
||||
"provider_settings.wake_prefix": {
|
||||
"description": "LLM 聊天额外唤醒前缀 ",
|
||||
"type": "string",
|
||||
"hint": "例子: 如果唤醒前缀为 `/`, 额外聊天唤醒前缀为 `chat`,则需要 `/chat` 才会触发 LLM 请求。默认为空。",
|
||||
},
|
||||
"provider_settings.prompt_prefix": {
|
||||
"description": "额外前缀提示词",
|
||||
"description": "用户提示词",
|
||||
"type": "string",
|
||||
"hint": "可使用 {{prompt}} 作为用户输入的占位符。如果不输入占位符则代表添加在用户输入的前面。",
|
||||
},
|
||||
"provider_tts_settings.dual_output": {
|
||||
"description": "开启 TTS 时同时输出语音和文字内容",
|
||||
|
||||
@@ -190,6 +190,16 @@ class RespondStage(Stage):
|
||||
except Exception as e:
|
||||
logger.warning(f"空内容检查异常: {e}")
|
||||
|
||||
# 将 Plain 为空的消息段移除
|
||||
result.chain = [
|
||||
comp
|
||||
for comp in result.chain
|
||||
if not (
|
||||
isinstance(comp, Comp.Plain)
|
||||
and (not comp.text or not comp.text.strip())
|
||||
)
|
||||
]
|
||||
|
||||
# 发送消息链
|
||||
# Record 需要强制单独发送
|
||||
need_separately = {ComponentType.Record}
|
||||
|
||||
@@ -14,3 +14,5 @@ class PlatformMetadata:
|
||||
"""平台的默认配置模板"""
|
||||
adapter_display_name: str = None
|
||||
"""显示在 WebUI 配置页中的平台名称,如空则是 name"""
|
||||
logo_path: str = None
|
||||
"""平台适配器的 logo 文件路径(相对于插件目录)"""
|
||||
|
||||
@@ -13,10 +13,12 @@ def register_platform_adapter(
|
||||
desc: str,
|
||||
default_config_tmpl: dict = None,
|
||||
adapter_display_name: str = None,
|
||||
logo_path: str = None,
|
||||
):
|
||||
"""用于注册平台适配器的带参装饰器。
|
||||
|
||||
default_config_tmpl 指定了平台适配器的默认配置模板。用户填写好后将会作为 platform_config 传入你的 Platform 类的实现类。
|
||||
logo_path 指定了平台适配器的 logo 文件路径,是相对于插件目录的路径。
|
||||
"""
|
||||
|
||||
def decorator(cls):
|
||||
@@ -39,6 +41,7 @@ def register_platform_adapter(
|
||||
description=desc,
|
||||
default_config_tmpl=default_config_tmpl,
|
||||
adapter_display_name=adapter_display_name,
|
||||
logo_path=logo_path,
|
||||
)
|
||||
platform_registry.append(pm)
|
||||
platform_cls_map[adapter_name] = cls
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from astrbot import logger
|
||||
from astrbot.core.provider.func_tool_manager import FuncCall
|
||||
from typing import List
|
||||
# This file was originally created to adapt to glm-4v-flash, which only supports one image in the context.
|
||||
# It is no longer specifically adapted to Zhipu's models. To ensure compatibility, this
|
||||
|
||||
|
||||
from ..register import register_provider_adapter
|
||||
from astrbot.core.provider.entities import LLMResponse
|
||||
from .openai_source import ProviderOpenAIOfficial
|
||||
|
||||
|
||||
@register_provider_adapter("zhipu_chat_completion", "智浦 Chat Completion 提供商适配器")
|
||||
@register_provider_adapter("zhipu_chat_completion", "智谱 Chat Completion 提供商适配器")
|
||||
class ProviderZhipu(ProviderOpenAIOfficial):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -19,63 +19,3 @@ class ProviderZhipu(ProviderOpenAIOfficial):
|
||||
provider_settings,
|
||||
default_persona,
|
||||
)
|
||||
|
||||
async def text_chat(
|
||||
self,
|
||||
prompt: str,
|
||||
session_id: str = None,
|
||||
image_urls: List[str] = None,
|
||||
func_tool: FuncCall = None,
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
if contexts is None:
|
||||
contexts = []
|
||||
new_record = await self.assemble_context(prompt, image_urls)
|
||||
context_query = []
|
||||
|
||||
context_query = [*contexts, new_record]
|
||||
|
||||
model_cfgs: dict = self.provider_config.get("model_config", {})
|
||||
model = model or self.get_model()
|
||||
# glm-4v-flash 只支持一张图片
|
||||
if model.lower() == "glm-4v-flash" and image_urls and len(context_query) > 1:
|
||||
logger.debug("glm-4v-flash 只支持一张图片,将只保留最后一张图片")
|
||||
logger.debug(context_query)
|
||||
new_context_query_ = []
|
||||
for i in range(0, len(context_query) - 1, 2):
|
||||
if isinstance(context_query[i].get("content", ""), list):
|
||||
continue
|
||||
new_context_query_.append(context_query[i])
|
||||
new_context_query_.append(context_query[i + 1])
|
||||
new_context_query_.append(context_query[-1]) # 保留最后一条记录
|
||||
context_query = new_context_query_
|
||||
logger.debug(context_query)
|
||||
|
||||
if system_prompt:
|
||||
context_query.insert(0, {"role": "system", "content": system_prompt})
|
||||
|
||||
payloads = {"messages": context_query, **model_cfgs}
|
||||
try:
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
return llm_response
|
||||
except Exception as e:
|
||||
if "maximum context length" in str(e):
|
||||
retry_cnt = 10
|
||||
while retry_cnt > 0:
|
||||
logger.warning(
|
||||
f"请求失败:{e}。上下文长度超过限制。尝试弹出最早的记录然后重试。"
|
||||
)
|
||||
try:
|
||||
self.pop_record(session_id)
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
break
|
||||
except Exception as e:
|
||||
if "maximum context length" in str(e):
|
||||
retry_cnt -= 1
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise e
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
import traceback
|
||||
import os
|
||||
import inspect
|
||||
from .route import Route, Response, RouteContext
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from quart import request
|
||||
@@ -13,10 +14,10 @@ from astrbot.core.config.default import (
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.platform.register import platform_registry
|
||||
from astrbot.core.platform.register import platform_registry, platform_cls_map
|
||||
from astrbot.core.provider.register import provider_registry
|
||||
from astrbot.core.star.star import star_registry
|
||||
from astrbot.core import logger
|
||||
from astrbot.core import logger, file_token_service
|
||||
from astrbot.core.provider import Provider
|
||||
from astrbot.core.provider.provider import RerankProvider
|
||||
import asyncio
|
||||
@@ -149,6 +150,7 @@ class ConfigRoute(Route):
|
||||
super().__init__(context)
|
||||
self.core_lifecycle = core_lifecycle
|
||||
self.config: AstrBotConfig = core_lifecycle.astrbot_config
|
||||
self._logo_token_cache = {} # 缓存logo token,避免重复注册
|
||||
self.acm = core_lifecycle.astrbot_config_mgr
|
||||
self.routes = {
|
||||
"/config/abconf/new": ("POST", self.create_abconf),
|
||||
@@ -655,6 +657,78 @@ class ConfigRoute(Route):
|
||||
return Response().error(str(e)).__dict__
|
||||
return Response().ok(None, "删除成功,已经实时生效~").__dict__
|
||||
|
||||
async def get_llm_tools(self):
|
||||
"""获取函数调用工具。包含了本地加载的以及 MCP 服务的工具"""
|
||||
tool_mgr = self.core_lifecycle.provider_manager.llm_tools
|
||||
tools = tool_mgr.get_func_desc_openai_style()
|
||||
return Response().ok(tools).__dict__
|
||||
|
||||
async def _register_platform_logo(self, platform, platform_default_tmpl):
|
||||
"""注册平台logo文件并生成访问令牌"""
|
||||
if not platform.logo_path:
|
||||
return
|
||||
|
||||
try:
|
||||
# 检查缓存
|
||||
cache_key = f"{platform.name}:{platform.logo_path}"
|
||||
if cache_key in self._logo_token_cache:
|
||||
cached_token = self._logo_token_cache[cache_key]
|
||||
# 确保platform_default_tmpl[platform.name]存在且为字典
|
||||
if platform.name not in platform_default_tmpl:
|
||||
platform_default_tmpl[platform.name] = {}
|
||||
elif not isinstance(platform_default_tmpl[platform.name], dict):
|
||||
platform_default_tmpl[platform.name] = {}
|
||||
platform_default_tmpl[platform.name]["logo_token"] = cached_token
|
||||
logger.debug(f"Using cached logo token for platform {platform.name}")
|
||||
return
|
||||
|
||||
# 获取平台适配器类
|
||||
platform_cls = platform_cls_map.get(platform.name)
|
||||
if not platform_cls:
|
||||
logger.warning(f"Platform class not found for {platform.name}")
|
||||
return
|
||||
|
||||
# 获取插件目录路径
|
||||
module_file = inspect.getfile(platform_cls)
|
||||
plugin_dir = os.path.dirname(module_file)
|
||||
|
||||
# 解析logo文件路径
|
||||
logo_file_path = os.path.join(plugin_dir, platform.logo_path)
|
||||
|
||||
# 检查文件是否存在并注册令牌
|
||||
if os.path.exists(logo_file_path):
|
||||
logo_token = await file_token_service.register_file(
|
||||
logo_file_path, timeout=3600
|
||||
)
|
||||
|
||||
# 确保platform_default_tmpl[platform.name]存在且为字典
|
||||
if platform.name not in platform_default_tmpl:
|
||||
platform_default_tmpl[platform.name] = {}
|
||||
elif not isinstance(platform_default_tmpl[platform.name], dict):
|
||||
platform_default_tmpl[platform.name] = {}
|
||||
|
||||
platform_default_tmpl[platform.name]["logo_token"] = logo_token
|
||||
|
||||
# 缓存token
|
||||
self._logo_token_cache[cache_key] = logo_token
|
||||
|
||||
logger.debug(f"Logo token registered for platform {platform.name}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Platform {platform.name} logo file not found: {logo_file_path}"
|
||||
)
|
||||
|
||||
except (ImportError, AttributeError) as e:
|
||||
logger.warning(
|
||||
f"Failed to import required modules for platform {platform.name}: {e}"
|
||||
)
|
||||
except (OSError, IOError) as e:
|
||||
logger.warning(f"File system error for platform {platform.name} logo: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Unexpected error registering logo for platform {platform.name}: {e}"
|
||||
)
|
||||
|
||||
async def _get_astrbot_config(self):
|
||||
config = self.config
|
||||
|
||||
@@ -662,9 +736,21 @@ class ConfigRoute(Route):
|
||||
platform_default_tmpl = CONFIG_METADATA_2["platform_group"]["metadata"][
|
||||
"platform"
|
||||
]["config_template"]
|
||||
|
||||
# 收集需要注册logo的平台
|
||||
logo_registration_tasks = []
|
||||
for platform in platform_registry:
|
||||
if platform.default_config_tmpl:
|
||||
platform_default_tmpl[platform.name] = platform.default_config_tmpl
|
||||
# 收集logo注册任务
|
||||
if platform.logo_path:
|
||||
logo_registration_tasks.append(
|
||||
self._register_platform_logo(platform, platform_default_tmpl)
|
||||
)
|
||||
|
||||
# 并行执行logo注册
|
||||
if logo_registration_tasks:
|
||||
await asyncio.gather(*logo_registration_tasks, return_exceptions=True)
|
||||
|
||||
# 服务提供商的默认配置模板注入
|
||||
provider_default_tmpl = CONFIG_METADATA_2["provider_group"]["metadata"][
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# What's Changed
|
||||
|
||||
1. fix: 修复"开启 TTS 时同时输出语音和文字内容"功能不可用的问题 ([#2900](https://github.com/AstrBotDevs/AstrBot/issues/2900))
|
||||
2. feat: 优化了会话管理页的数据查询逻辑,添加分页和搜索功能,大幅度提高响应速度 ([#2906](https://github.com/AstrBotDevs/AstrBot/issues/2906))
|
||||
3. fix: 用 mi-googlesearch-python 库代替失效的 googlesearch-python 库 ([#2909](https://github.com/AstrBotDevs/AstrBot/issues/2909))
|
||||
4. feat: 支持在 Telegram 和飞书下请求 LLM 前预表态功能 ([#2737](https://github.com/AstrBotDevs/AstrBot/issues/2737))
|
||||
5. perf: 对于 Telegram 群聊,将回复机器人的消息视为唤醒机器人 ([#2926](https://github.com/AstrBotDevs/AstrBot/issues/2926))
|
||||
6. feat: 提示词前缀配置项升级为“用户提示词”,支持 `{{prompt}}` 作为用户输入的占位符。
|
||||
7. fix: 增加知识库插件的启用检查,避免部分情况下导致知识库页面白屏的问题。
|
||||
8. fix: 修复接入智谱提供商后,工具调用无限循环的问题,并停止支持 glm-4v-flash ([#2931](https://github.com/AstrBotDevs/AstrBot/issues/2931))
|
||||
9. fix: 修复注册指令组指令时的 Pyright 类型检查提示 ([#2923](https://github.com/AstrBotDevs/AstrBot/issues/2923))
|
||||
10. refactor: 优化 packages/astrbot 内置插件的代码结构以提高可维护性和可读性 ([#2924](https://github.com/AstrBotDevs/AstrBot/issues/2924))
|
||||
11. fix: 修复插件指令注解为联合类型时处理异常的问题 ([#2925](https://github.com/AstrBotDevs/AstrBot/issues/2925))
|
||||
12. feat: 支持注册消息平台适配器的 logo ([#2109](https://github.com/AstrBotDevs/AstrBot/issues/2109))
|
||||
@@ -0,0 +1 @@
|
||||
# What's Changed
|
||||
@@ -114,7 +114,7 @@
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="handleIdConflictConfirm(false)">{{ tm('dialog.idConflict.confirm')
|
||||
}}</v-btn>
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -241,7 +241,15 @@ export default {
|
||||
|
||||
methods: {
|
||||
// 从工具函数导入
|
||||
getPlatformIcon,
|
||||
getPlatformIcon(platform_id) {
|
||||
// 首先检查是否有来自插件的 logo_token
|
||||
const template = this.metadata['platform_group']?.metadata?.platform?.config_template?.[platform_id];
|
||||
if (template && template.logo_token) {
|
||||
// 通过文件服务访问插件提供的 logo
|
||||
return `/api/file/${template.logo_token}`;
|
||||
}
|
||||
return getPlatformIcon(platform_id);
|
||||
},
|
||||
|
||||
openTutorial() {
|
||||
const tutorialUrl = getTutorialLink(this.newSelectedPlatformConfig.type);
|
||||
|
||||
@@ -601,11 +601,11 @@ export default {
|
||||
checkPlugin() {
|
||||
axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base')
|
||||
.then(response => {
|
||||
if (response.data.status !== 'ok') {
|
||||
if (response.data.status !== 'ok' || response.data.data.length === 0) {
|
||||
this.showSnackbar(this.tm('messages.pluginNotAvailable'), 'error');
|
||||
return
|
||||
}
|
||||
if (!response.data.data.activated) {
|
||||
if (!response.data.data[0].activated) {
|
||||
this.showSnackbar(this.tm('messages.pluginNotActivated'), 'error');
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,7 +106,11 @@ class ProcessLLMRequest:
|
||||
|
||||
# prompt prefix
|
||||
if prefix := cfg.get("prompt_prefix"):
|
||||
req.prompt = prefix + req.prompt
|
||||
# 支持 {{prompt}} 作为用户输入的占位符
|
||||
if "{{prompt}}" in prefix:
|
||||
req.prompt = prefix.replace("{{prompt}}", req.prompt)
|
||||
else:
|
||||
req.prompt = prefix + req.prompt
|
||||
|
||||
# user identifier
|
||||
if cfg.get("identifier"):
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.2.1"
|
||||
version = "4.3.1"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user