Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff998fdd8d | |||
| d7461ed54c | |||
| 3ce577acf9 | |||
| 50b1dccff3 | |||
| c33e7e30d4 | |||
| bc7f01ba36 | |||
| 2ce653caad | |||
| 0d850d7b22 | |||
| a2be155b8e | |||
| 68aa107689 | |||
| 23096ed3a5 | |||
| 90a65c35c1 | |||
| 3d88827a95 | |||
| 40a0a8df5a | |||
| 20f7129c0b | |||
| 0e962e95dd |
@@ -26,12 +26,13 @@ body:
|
||||
value: |
|
||||
```json
|
||||
{
|
||||
"name": "插件名",
|
||||
"desc": "插件介绍",
|
||||
"name": "插件名,请以 astrbot_plugin_ 开头",
|
||||
"display_name": "用于展示的插件名,方便人类阅读",
|
||||
"desc": "插件的简短介绍",
|
||||
"author": "作者名",
|
||||
"repo": "插件仓库链接",
|
||||
"tags": [],
|
||||
"social_link": ""
|
||||
"social_link": "",
|
||||
}
|
||||
```
|
||||
validations:
|
||||
|
||||
@@ -6,13 +6,13 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您抽出时间报告问题!请准确解释您的问题。如果可能,请提供一个可复现的片段(这有助于更快地解决问题)。
|
||||
感谢您抽出时间报告问题!请准确解释您的问题。如果可能,请提供一个可复现的片段(这有助于更快地解决问题)。请注意,不详细 / 没有日志的 issue 会被直接关闭,谢谢理解。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 发生了什么
|
||||
description: 描述你遇到的异常
|
||||
placeholder: >
|
||||
一个清晰且具体的描述这个异常是什么。
|
||||
一个清晰且具体的描述这个异常是什么。请注意,不详细 / 没有日志的 issue 会被直接关闭,谢谢理解。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -55,7 +55,7 @@ body:
|
||||
attributes:
|
||||
label: 报错日志
|
||||
description: >
|
||||
如报错日志、截图等。请提供完整的 Debug 级别的日志,不要介意它很长!
|
||||
如报错日志、截图等。请提供完整的 Debug 级别的日志,不要介意它很长!请注意,不详细 / 没有日志的 issue 会被直接关闭,谢谢理解。
|
||||
placeholder: >
|
||||
请提供完整的报错日志或截图。
|
||||
validations:
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
zip -r dist.zip dist
|
||||
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: dist-without-markdown
|
||||
path: |
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<div>
|
||||
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=1" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.5.0"
|
||||
VERSION = "4.5.1"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -324,6 +324,10 @@ CONFIG_METADATA_2 = {
|
||||
# "type": "string",
|
||||
# "options": ["fullscreen", "embedded"],
|
||||
# },
|
||||
"is_sandbox": {
|
||||
"description": "沙箱模式",
|
||||
"type": "bool",
|
||||
},
|
||||
"satori_api_base_url": {
|
||||
"description": "Satori API 终结点",
|
||||
"type": "string",
|
||||
@@ -767,6 +771,7 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "grok-2-latest", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"xai_native_search": False,
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"Anthropic": {
|
||||
@@ -1258,8 +1263,38 @@ CONFIG_METADATA_2 = {
|
||||
"rerank_model": "BAAI/bge-reranker-base",
|
||||
"timeout": 20,
|
||||
},
|
||||
"Xinference Rerank": {
|
||||
"id": "xinference_rerank",
|
||||
"type": "xinference_rerank",
|
||||
"provider": "xinference",
|
||||
"provider_type": "rerank",
|
||||
"enable": True,
|
||||
"rerank_api_key": "",
|
||||
"rerank_api_base": "http://127.0.0.1:9997",
|
||||
"rerank_model": "BAAI/bge-reranker-base",
|
||||
"timeout": 20,
|
||||
"launch_model_if_not_running": False,
|
||||
},
|
||||
"Xinference STT": {
|
||||
"id": "xinference_stt",
|
||||
"type": "xinference_stt",
|
||||
"provider": "xinference",
|
||||
"provider_type": "speech_to_text",
|
||||
"enable": False,
|
||||
"api_key": "",
|
||||
"api_base": "http://127.0.0.1:9997",
|
||||
"model": "whisper-large-v3",
|
||||
"timeout": 180,
|
||||
"launch_model_if_not_running": False,
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
"xai_native_search": {
|
||||
"description": "启用原生搜索功能",
|
||||
"type": "bool",
|
||||
"hint": "启用后,将通过 xAI 的 Chat Completions 原生 Live Search 进行联网检索(按需计费)。仅对 xAI 提供商生效。",
|
||||
"condition": {"provider": "xai"},
|
||||
},
|
||||
"rerank_api_base": {
|
||||
"description": "重排序模型 API Base URL",
|
||||
"type": "string",
|
||||
@@ -1274,6 +1309,11 @@ CONFIG_METADATA_2 = {
|
||||
"description": "重排序模型名称",
|
||||
"type": "string",
|
||||
},
|
||||
"launch_model_if_not_running": {
|
||||
"description": "模型未运行时自动启动",
|
||||
"type": "bool",
|
||||
"hint": "如果模型当前未在 Xinference 服务中运行,是否尝试自动启动它。在生产环境中建议关闭。",
|
||||
},
|
||||
"modalities": {
|
||||
"description": "模型能力",
|
||||
"type": "list",
|
||||
|
||||
@@ -17,8 +17,11 @@ async def check_migration_needed_v4(db_helper: BaseDatabase) -> bool:
|
||||
检查是否需要进行数据库迁移
|
||||
如果存在 data_v3.db 并且 preference 中没有 migration_done_v4,则需要进行迁移。
|
||||
"""
|
||||
data_v3_exists = os.path.exists(get_astrbot_data_path())
|
||||
if not data_v3_exists:
|
||||
# 仅当 data 目录下存在旧版本数据(data_v3.db 文件)时才考虑迁移
|
||||
data_dir = get_astrbot_data_path()
|
||||
data_v3_db = os.path.join(data_dir, "data_v3.db")
|
||||
|
||||
if not os.path.exists(data_v3_db):
|
||||
return False
|
||||
migration_done = await db_helper.get_preference(
|
||||
"global", "global", "migration_done_v4"
|
||||
@@ -32,7 +35,7 @@ async def do_migration_v4(
|
||||
db_helper: BaseDatabase,
|
||||
platform_id_map: dict[str, dict[str, str]],
|
||||
astrbot_config: AstrBotConfig,
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
执行数据库迁移
|
||||
迁移旧的 webchat_conversation 表到新的 conversation 表。
|
||||
|
||||
@@ -100,8 +100,11 @@ class DingtalkPlatformAdapter(Platform):
|
||||
abm.raw_message = message
|
||||
|
||||
if abm.type == MessageType.GROUP_MESSAGE:
|
||||
if message.is_in_at_list:
|
||||
abm.message.append(At(qq=abm.self_id))
|
||||
# 处理所有被 @ 的用户(包括机器人自己,因 at_users 已包含)
|
||||
if message.at_users:
|
||||
for user in message.at_users:
|
||||
if user.dingtalk_id:
|
||||
abm.message.append(At(qq=user.dingtalk_id))
|
||||
abm.group_id = message.conversation_id
|
||||
if self.unique_session:
|
||||
abm.session_id = abm.sender.user_id
|
||||
|
||||
@@ -259,6 +259,10 @@ class ProviderManager:
|
||||
from .sources.whisper_selfhosted_source import (
|
||||
ProviderOpenAIWhisperSelfHost as ProviderOpenAIWhisperSelfHost,
|
||||
)
|
||||
case "xinference_stt":
|
||||
from .sources.xinference_stt_provider import (
|
||||
ProviderXinferenceSTT as ProviderXinferenceSTT,
|
||||
)
|
||||
case "openai_tts_api":
|
||||
from .sources.openai_tts_api_source import (
|
||||
ProviderOpenAITTSAPI as ProviderOpenAITTSAPI,
|
||||
@@ -311,6 +315,10 @@ class ProviderManager:
|
||||
from .sources.vllm_rerank_source import (
|
||||
VLLMRerankProvider as VLLMRerankProvider,
|
||||
)
|
||||
case "xinference_rerank":
|
||||
from .sources.xinference_rerank_source import (
|
||||
XinferenceRerankProvider as XinferenceRerankProvider,
|
||||
)
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
logger.critical(
|
||||
f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。"
|
||||
|
||||
@@ -68,6 +68,28 @@ class ProviderOpenAIOfficial(Provider):
|
||||
model = model_config.get("model", "unknown")
|
||||
self.set_model(model)
|
||||
|
||||
def _maybe_inject_xai_search(self, payloads: dict, **kwargs):
|
||||
"""当开启 xAI 原生搜索时,向请求体注入 Live Search 参数。
|
||||
|
||||
- 仅在 provider_config.xai_native_search 为 True 时生效
|
||||
- 默认注入 {"mode": "auto"}
|
||||
- 允许通过 kwargs 使用 xai_search_mode 覆盖(on/auto/off)
|
||||
"""
|
||||
if not bool(self.provider_config.get("xai_native_search", False)):
|
||||
return
|
||||
|
||||
mode = kwargs.get("xai_search_mode", "auto")
|
||||
mode = str(mode).lower()
|
||||
if mode not in ("auto", "on", "off"):
|
||||
mode = "auto"
|
||||
|
||||
# off 时不注入,保持与未开启一致
|
||||
if mode == "off":
|
||||
return
|
||||
|
||||
# OpenAI SDK 不识别的字段会在 _query/_query_stream 中放入 extra_body
|
||||
payloads["search_parameters"] = {"mode": mode}
|
||||
|
||||
async def get_models(self):
|
||||
try:
|
||||
models_str = []
|
||||
@@ -271,6 +293,9 @@ class ProviderOpenAIOfficial(Provider):
|
||||
|
||||
payloads = {"messages": context_query, **model_config}
|
||||
|
||||
# xAI 原生搜索参数(最小侵入地在此处注入)
|
||||
self._maybe_inject_xai_search(payloads, **kwargs)
|
||||
|
||||
return payloads, context_query
|
||||
|
||||
async def _handle_api_error(
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
from xinference_client.client.restful.async_restful_client import (
|
||||
AsyncClient as Client,
|
||||
)
|
||||
from astrbot import logger
|
||||
from ..provider import RerankProvider
|
||||
from ..register import register_provider_adapter
|
||||
from ..entities import ProviderType, RerankResult
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
"xinference_rerank",
|
||||
"Xinference Rerank 适配器",
|
||||
provider_type=ProviderType.RERANK,
|
||||
)
|
||||
class XinferenceRerankProvider(RerankProvider):
|
||||
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.base_url = provider_config.get("rerank_api_base", "http://127.0.0.1:8000")
|
||||
self.base_url = self.base_url.rstrip("/")
|
||||
self.timeout = provider_config.get("timeout", 20)
|
||||
self.model_name = provider_config.get("rerank_model", "BAAI/bge-reranker-base")
|
||||
self.api_key = provider_config.get("rerank_api_key")
|
||||
self.launch_model_if_not_running = provider_config.get(
|
||||
"launch_model_if_not_running", False
|
||||
)
|
||||
self.client = None
|
||||
self.model = None
|
||||
self.model_uid = None
|
||||
|
||||
async def initialize(self):
|
||||
if self.api_key:
|
||||
logger.info("Xinference Rerank: Using API key for authentication.")
|
||||
self.client = Client(self.base_url, api_key=self.api_key)
|
||||
else:
|
||||
logger.info("Xinference Rerank: No API key provided.")
|
||||
self.client = Client(self.base_url)
|
||||
|
||||
try:
|
||||
running_models = await self.client.list_models()
|
||||
for uid, model_spec in running_models.items():
|
||||
if model_spec.get("model_name") == self.model_name:
|
||||
logger.info(
|
||||
f"Model '{self.model_name}' is already running with UID: {uid}"
|
||||
)
|
||||
self.model_uid = uid
|
||||
break
|
||||
|
||||
if self.model_uid is None:
|
||||
if self.launch_model_if_not_running:
|
||||
logger.info(f"Launching {self.model_name} model...")
|
||||
self.model_uid = await self.client.launch_model(
|
||||
model_name=self.model_name, model_type="rerank"
|
||||
)
|
||||
logger.info("Model launched.")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Model '{self.model_name}' is not running and auto-launch is disabled. Provider will not be available."
|
||||
)
|
||||
return
|
||||
|
||||
if self.model_uid:
|
||||
self.model = await self.client.get_model(self.model_uid)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Xinference model: {e}")
|
||||
logger.debug(
|
||||
f"Xinference initialization failed with exception: {e}", exc_info=True
|
||||
)
|
||||
self.model = None
|
||||
|
||||
async def rerank(
|
||||
self, query: str, documents: list[str], top_n: int | None = None
|
||||
) -> list[RerankResult]:
|
||||
if not self.model:
|
||||
logger.error("Xinference rerank model is not initialized.")
|
||||
return []
|
||||
try:
|
||||
response = await self.model.rerank(documents, query, top_n)
|
||||
results = response.get("results", [])
|
||||
logger.debug(f"Rerank API response: {response}")
|
||||
|
||||
if not results:
|
||||
logger.warning(
|
||||
f"Rerank API returned an empty list. Original response: {response}"
|
||||
)
|
||||
|
||||
return [
|
||||
RerankResult(
|
||||
index=result["index"],
|
||||
relevance_score=result["relevance_score"],
|
||||
)
|
||||
for result in results
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Xinference rerank failed: {e}")
|
||||
logger.debug(f"Xinference rerank failed with exception: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
async def terminate(self) -> None:
|
||||
"""关闭客户端会话"""
|
||||
if self.client:
|
||||
logger.info("Closing Xinference rerank client...")
|
||||
try:
|
||||
await self.client.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to close Xinference client: {e}", exc_info=True)
|
||||
@@ -0,0 +1,187 @@
|
||||
import uuid
|
||||
import os
|
||||
import aiohttp
|
||||
from xinference_client.client.restful.async_restful_client import (
|
||||
AsyncClient as Client,
|
||||
)
|
||||
from ..provider import STTProvider
|
||||
from ..entities import ProviderType
|
||||
from ..register import register_provider_adapter
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.utils.tencent_record_helper import tencent_silk_to_wav
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
"xinference_stt",
|
||||
"Xinference STT",
|
||||
provider_type=ProviderType.SPEECH_TO_TEXT,
|
||||
)
|
||||
class ProviderXinferenceSTT(STTProvider):
|
||||
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.base_url = provider_config.get("api_base", "http://127.0.0.1:9997")
|
||||
self.base_url = self.base_url.rstrip("/")
|
||||
self.timeout = provider_config.get("timeout", 180)
|
||||
self.model_name = provider_config.get("model", "whisper-large-v3")
|
||||
self.api_key = provider_config.get("api_key")
|
||||
self.launch_model_if_not_running = provider_config.get(
|
||||
"launch_model_if_not_running", False
|
||||
)
|
||||
self.client = None
|
||||
self.model_uid = None
|
||||
|
||||
async def initialize(self):
|
||||
if self.api_key:
|
||||
logger.info("Xinference STT: Using API key for authentication.")
|
||||
self.client = Client(self.base_url, api_key=self.api_key)
|
||||
else:
|
||||
logger.info("Xinference STT: No API key provided.")
|
||||
self.client = Client(self.base_url)
|
||||
|
||||
try:
|
||||
running_models = await self.client.list_models()
|
||||
for uid, model_spec in running_models.items():
|
||||
if model_spec.get("model_name") == self.model_name:
|
||||
logger.info(
|
||||
f"Model '{self.model_name}' is already running with UID: {uid}"
|
||||
)
|
||||
self.model_uid = uid
|
||||
break
|
||||
|
||||
if self.model_uid is None:
|
||||
if self.launch_model_if_not_running:
|
||||
logger.info(f"Launching {self.model_name} model...")
|
||||
self.model_uid = await self.client.launch_model(
|
||||
model_name=self.model_name, model_type="audio"
|
||||
)
|
||||
logger.info("Model launched.")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Model '{self.model_name}' is not running and auto-launch is disabled. Provider will not be available."
|
||||
)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Xinference model: {e}")
|
||||
logger.debug(
|
||||
f"Xinference initialization failed with exception: {e}", exc_info=True
|
||||
)
|
||||
|
||||
async def get_text(self, audio_url: str) -> str:
|
||||
if not self.model_uid or self.client is None or self.client.session is None:
|
||||
logger.error("Xinference STT model is not initialized.")
|
||||
return ""
|
||||
|
||||
audio_bytes = None
|
||||
temp_files = []
|
||||
is_tencent = False
|
||||
|
||||
try:
|
||||
# 1. Get audio bytes
|
||||
if audio_url.startswith("http"):
|
||||
if "multimedia.nt.qq.com.cn" in audio_url:
|
||||
is_tencent = True
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(audio_url, timeout=self.timeout) as resp:
|
||||
if resp.status == 200:
|
||||
audio_bytes = await resp.read()
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed to download audio from {audio_url}, status: {resp.status}"
|
||||
)
|
||||
return ""
|
||||
else:
|
||||
if os.path.exists(audio_url):
|
||||
with open(audio_url, "rb") as f:
|
||||
audio_bytes = f.read()
|
||||
else:
|
||||
logger.error(f"File not found: {audio_url}")
|
||||
return ""
|
||||
|
||||
if not audio_bytes:
|
||||
logger.error("Audio bytes are empty.")
|
||||
return ""
|
||||
|
||||
# 2. Check for conversion
|
||||
needs_conversion = False
|
||||
if (
|
||||
audio_url.endswith((".amr", ".silk"))
|
||||
or is_tencent
|
||||
or b"SILK" in audio_bytes[:8]
|
||||
):
|
||||
needs_conversion = True
|
||||
|
||||
# 3. Perform conversion if needed
|
||||
if needs_conversion:
|
||||
logger.info("Audio requires conversion, using temporary files...")
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
input_path = os.path.join(temp_dir, str(uuid.uuid4()))
|
||||
output_path = os.path.join(temp_dir, str(uuid.uuid4()) + ".wav")
|
||||
temp_files.extend([input_path, output_path])
|
||||
|
||||
with open(input_path, "wb") as f:
|
||||
f.write(audio_bytes)
|
||||
|
||||
logger.info("Converting silk/amr file to wav ...")
|
||||
await tencent_silk_to_wav(input_path, output_path)
|
||||
|
||||
with open(output_path, "rb") as f:
|
||||
audio_bytes = f.read()
|
||||
|
||||
# 4. Transcribe
|
||||
# 官方asyncCLient的客户端似乎实现有点问题,这里直接用aiohttp实现openai标准兼容请求,提交issue等待官方修复后再改回来
|
||||
url = f"{self.base_url}/v1/audio/transcriptions"
|
||||
headers = {
|
||||
"accept": "application/json",
|
||||
}
|
||||
if self.client and self.client._headers:
|
||||
headers.update(self.client._headers)
|
||||
|
||||
data = aiohttp.FormData()
|
||||
data.add_field("model", self.model_uid)
|
||||
data.add_field(
|
||||
"file", audio_bytes, filename="audio.wav", content_type="audio/wav"
|
||||
)
|
||||
|
||||
async with self.client.session.post(
|
||||
url, data=data, headers=headers, timeout=self.timeout
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
result = await resp.json()
|
||||
text = result.get("text", "")
|
||||
logger.debug(f"Xinference STT result: {text}")
|
||||
return text
|
||||
else:
|
||||
error_text = await resp.text()
|
||||
logger.error(
|
||||
f"Xinference STT transcription failed with status {resp.status}: {error_text}"
|
||||
)
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Xinference STT failed: {e}")
|
||||
logger.debug(f"Xinference STT failed with exception: {e}", exc_info=True)
|
||||
return ""
|
||||
finally:
|
||||
# 5. Cleanup
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
logger.debug(f"Removed temporary file: {temp_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove temporary file {temp_file}: {e}")
|
||||
|
||||
async def terminate(self) -> None:
|
||||
"""关闭客户端会话"""
|
||||
if self.client:
|
||||
logger.info("Closing Xinference STT client...")
|
||||
try:
|
||||
await self.client.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to close Xinference client: {e}", exc_info=True)
|
||||
@@ -1,5 +1,4 @@
|
||||
from asyncio import Queue
|
||||
from typing import List, Union
|
||||
|
||||
from astrbot.core.provider.provider import (
|
||||
Provider,
|
||||
@@ -11,7 +10,7 @@ from astrbot.core.provider.provider import (
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.provider.func_tool_manager import FunctionToolManager
|
||||
from astrbot.core.provider.func_tool_manager import FunctionToolManager, FunctionTool
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.provider.manager import ProviderManager
|
||||
@@ -25,7 +24,8 @@ from .star import star_registry, StarMetadata, star_map
|
||||
from .star_handler import star_handlers_registry, StarHandlerMetadata, EventType
|
||||
from .filter.command import CommandFilter
|
||||
from .filter.regex import RegexFilter
|
||||
from typing import Awaitable, Any, Callable
|
||||
from typing import Any
|
||||
from collections.abc import Awaitable, Callable
|
||||
from astrbot.core.conversation_mgr import ConversationManager
|
||||
from astrbot.core.star.filter.platform_adapter_type import (
|
||||
PlatformAdapterType,
|
||||
@@ -42,7 +42,7 @@ class Context:
|
||||
registered_web_apis: list = []
|
||||
|
||||
# back compatibility
|
||||
_register_tasks: List[Awaitable] = []
|
||||
_register_tasks: list[Awaitable] = []
|
||||
_star_manager = None
|
||||
|
||||
def __init__(
|
||||
@@ -78,7 +78,7 @@ class Context:
|
||||
if star.name == star_name:
|
||||
return star
|
||||
|
||||
def get_all_stars(self) -> List[StarMetadata]:
|
||||
def get_all_stars(self) -> list[StarMetadata]:
|
||||
"""获取当前载入的所有插件 Metadata 的列表"""
|
||||
return star_registry
|
||||
|
||||
@@ -116,19 +116,19 @@ class Context:
|
||||
prov = self.provider_manager.inst_map.get(provider_id)
|
||||
return prov
|
||||
|
||||
def get_all_providers(self) -> List[Provider]:
|
||||
def get_all_providers(self) -> list[Provider]:
|
||||
"""获取所有用于文本生成任务的 LLM Provider(Chat_Completion 类型)。"""
|
||||
return self.provider_manager.provider_insts
|
||||
|
||||
def get_all_tts_providers(self) -> List[TTSProvider]:
|
||||
def get_all_tts_providers(self) -> list[TTSProvider]:
|
||||
"""获取所有用于 TTS 任务的 Provider。"""
|
||||
return self.provider_manager.tts_provider_insts
|
||||
|
||||
def get_all_stt_providers(self) -> List[STTProvider]:
|
||||
def get_all_stt_providers(self) -> list[STTProvider]:
|
||||
"""获取所有用于 STT 任务的 Provider。"""
|
||||
return self.provider_manager.stt_provider_insts
|
||||
|
||||
def get_all_embedding_providers(self) -> List[EmbeddingProvider]:
|
||||
def get_all_embedding_providers(self) -> list[EmbeddingProvider]:
|
||||
"""获取所有用于 Embedding 任务的 Provider。"""
|
||||
return self.provider_manager.embedding_provider_insts
|
||||
|
||||
@@ -196,9 +196,7 @@ class Context:
|
||||
return self._event_queue
|
||||
|
||||
@deprecated(version="4.0.0", reason="Use get_platform_inst instead")
|
||||
def get_platform(
|
||||
self, platform_type: Union[PlatformAdapterType, str]
|
||||
) -> Platform | None:
|
||||
def get_platform(self, platform_type: PlatformAdapterType | str) -> Platform | None:
|
||||
"""
|
||||
获取指定类型的平台适配器。
|
||||
|
||||
@@ -231,7 +229,7 @@ class Context:
|
||||
return platform
|
||||
|
||||
async def send_message(
|
||||
self, session: Union[str, MessageSesion], message_chain: MessageChain
|
||||
self, session: str | MessageSesion, message_chain: MessageChain
|
||||
) -> bool:
|
||||
"""
|
||||
根据 session(unified_msg_origin) 主动发送消息。
|
||||
@@ -258,6 +256,11 @@ class Context:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_llm_tools(self, *tools: FunctionTool) -> None:
|
||||
"""添加 LLM 工具。"""
|
||||
for tool in tools:
|
||||
self.provider_manager.llm_tools.func_list.append(tool)
|
||||
|
||||
"""
|
||||
以下的方法已经不推荐使用。请从 AstrBot 文档查看更好的注册方式。
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
## What's Changed
|
||||
|
||||
1. 修复:第一次启动时不再错误地弹出迁移提醒
|
||||
2. 新增:Xinference Rerank Provider, STT Provider
|
||||
3. 新增: xAI Grok Live Search
|
||||
4. 优化: 插件卡片左下角恢复 文档 按钮并新增 插件配置 按钮。
|
||||
5. 优化: 更好地适配 Class 方式注册 LLM Tool。
|
||||
@@ -202,13 +202,22 @@ const viewReadme = () => {
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="mt-2" :class="{ 'text-caption': $vuetify.display.xs }" style="overflow-y: auto; height: 80px; font-size: 90%;">
|
||||
<div class="mt-2" :class="{ 'text-caption': $vuetify.display.xs }" style="overflow-y: auto; height: 70px; font-size: 90%;">
|
||||
{{ extension.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="extension-actions">
|
||||
<v-btn color="primary" size="small" @click="viewReadme">
|
||||
{{ tm('buttons.viewDocs') }}
|
||||
</v-btn>
|
||||
<v-btn v-if="!marketMode" color="primary" size="small" @click="configure">
|
||||
{{ tm('card.actions.pluginConfig') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
</template>
|
||||
@@ -238,4 +247,9 @@ const viewReadme = () => {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.extension-actions {
|
||||
margin-top: auto;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -740,7 +740,7 @@ onMounted(async () => {
|
||||
<!-- 卡片视图 -->
|
||||
<div v-else>
|
||||
<v-row v-if="filteredPlugins.length === 0" class="text-center">
|
||||
<v-col cols="12" class="pa-8">
|
||||
<v-col cols="12" class="pa-2">
|
||||
<v-icon size="64" color="info" class="mb-4">mdi-puzzle-outline</v-icon>
|
||||
<div class="text-h5 mb-2">{{ tm('empty.noPlugins') }}</div>
|
||||
<div class="text-body-1 mb-4">{{ tm('empty.noPluginsDesc') }}</div>
|
||||
@@ -748,8 +748,8 @@ onMounted(async () => {
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" lg="6" v-for="extension in filteredPlugins" :key="extension.name"
|
||||
class="pb-4">
|
||||
<v-col cols="12" md="6" lg="4" v-for="extension in filteredPlugins" :key="extension.name"
|
||||
class="pb-2">
|
||||
<ExtensionCard :extension="extension" class="rounded-lg" style="background-color: rgb(var(--v-theme-mcpCardBg));"
|
||||
@configure="openExtensionConfig(extension.name)" @uninstall="uninstallExtension(extension.name)"
|
||||
@update="updateExtension(extension.name)" @reload="reloadPlugin(extension.name)"
|
||||
|
||||
@@ -159,7 +159,11 @@ class ProviderCommands:
|
||||
message.set_result(
|
||||
MessageEventResult().message("切换模型未知错误: " + str(e))
|
||||
)
|
||||
message.set_result(MessageEventResult().message("切换模型成功。"))
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"切换模型成功。当前提供商: [{prov.meta().id}] 当前模型: [{prov.get_model()}]"
|
||||
)
|
||||
)
|
||||
else:
|
||||
prov.set_model(idx_or_name)
|
||||
message.set_result(
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.5.0"
|
||||
version = "4.5.1"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -55,6 +55,7 @@ dependencies = [
|
||||
"rank-bm25>=0.2.2",
|
||||
"jieba>=0.42.1",
|
||||
"markitdown-no-magika[docx,xls,xlsx]>=0.1.2",
|
||||
"xinference-client",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
+52
-49
@@ -1,51 +1,54 @@
|
||||
aiohttp
|
||||
pydantic~=2.10.3
|
||||
psutil>=5.8.0
|
||||
openai
|
||||
anthropic
|
||||
qq-botpy
|
||||
aiocqhttp>=1.4.4
|
||||
aiodocker>=0.24.0
|
||||
aiohttp>=3.11.18
|
||||
aiocqhttp>=1.4.4
|
||||
aiodocker>=0.24.0
|
||||
aiohttp>=3.11.18
|
||||
aiosqlite>=0.21.0
|
||||
anthropic>=0.51.0
|
||||
apscheduler>=3.11.0
|
||||
beautifulsoup4>=4.13.4
|
||||
certifi>=2025.4.26
|
||||
chardet~=5.1.0
|
||||
Pillow
|
||||
beautifulsoup4
|
||||
mi-googlesearch-python
|
||||
readability-lxml
|
||||
quart
|
||||
lxml_html_clean
|
||||
colorlog
|
||||
aiocqhttp
|
||||
pyjwt
|
||||
apscheduler
|
||||
docstring_parser
|
||||
aiodocker
|
||||
silk-python
|
||||
lark-oapi
|
||||
ormsgpack
|
||||
cryptography
|
||||
dashscope
|
||||
python-telegram-bot
|
||||
wechatpy
|
||||
dingtalk-stream
|
||||
defusedxml
|
||||
mcp
|
||||
certifi
|
||||
pip
|
||||
telegramify-markdown
|
||||
google-genai
|
||||
click
|
||||
filelock
|
||||
watchfiles
|
||||
websockets
|
||||
faiss-cpu
|
||||
aiosqlite
|
||||
colorlog>=6.9.0
|
||||
cryptography>=44.0.3
|
||||
dashscope>=1.23.2
|
||||
defusedxml>=0.7.1
|
||||
deprecated>=1.2.18
|
||||
dingtalk-stream>=0.22.1
|
||||
docstring-parser>=0.16
|
||||
faiss-cpu==1.10.0
|
||||
filelock>=3.18.0
|
||||
google-genai>=1.14.0
|
||||
lark-oapi>=1.4.15
|
||||
lxml-html-clean>=0.4.2
|
||||
mcp>=1.8.0
|
||||
openai>=1.78.0
|
||||
ormsgpack>=1.9.1
|
||||
pillow>=11.2.1
|
||||
pip>=25.1.1
|
||||
psutil>=5.8.0
|
||||
py-cord>=2.6.1
|
||||
slack-sdk
|
||||
pydub
|
||||
sqlmodel
|
||||
deprecated
|
||||
sqlalchemy[asyncio]
|
||||
audioop-lts; python_version>='3.13'
|
||||
pypdf
|
||||
aiofiles
|
||||
rank-bm25
|
||||
jieba
|
||||
markitdown-no-magika[docx,xls,xlsx]
|
||||
pydantic~=2.10.3
|
||||
pydub>=0.25.1
|
||||
pyjwt>=2.10.1
|
||||
python-telegram-bot>=22.0
|
||||
qq-botpy>=1.2.1
|
||||
quart>=0.20.0
|
||||
readability-lxml>=0.8.4.1
|
||||
silk-python>=0.2.6
|
||||
slack-sdk>=3.35.0
|
||||
sqlalchemy[asyncio]>=2.0.41
|
||||
sqlmodel>=0.0.24
|
||||
telegramify-markdown>=0.5.1
|
||||
watchfiles>=1.0.5
|
||||
websockets>=15.0.1
|
||||
wechatpy>=1.8.18
|
||||
audioop-lts ; python_full_version >= '3.13'
|
||||
click>=8.2.1
|
||||
pypdf>=6.1.1
|
||||
aiofiles>=25.1.0
|
||||
rank-bm25>=0.2.2
|
||||
jieba>=0.42.1
|
||||
markitdown-no-magika[docx,xls,xlsx]>=0.1.2
|
||||
xinference-client
|
||||
|
||||
Reference in New Issue
Block a user