Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9cdf47603 | |||
| 55ac878648 | |||
| 60abddada3 | |||
| bbc583cc8d | |||
| 7906030037 | |||
| 06b385697d | |||
| 059008a903 |
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install UV
|
||||
run: pip install uv
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.17.2"
|
||||
__version__ = "4.17.3"
|
||||
|
||||
@@ -5,8 +5,9 @@ import mcp
|
||||
from astrbot.api import FunctionTool
|
||||
from astrbot.core.agent.run_context import ContextWrapper
|
||||
from astrbot.core.agent.tool import ToolExecResult
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext, AstrMessageEvent
|
||||
from astrbot.core.computer.computer_client import get_booter, get_local_booter
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
|
||||
param_schema = {
|
||||
"type": "object",
|
||||
@@ -25,7 +26,7 @@ param_schema = {
|
||||
}
|
||||
|
||||
|
||||
def handle_result(result: dict) -> ToolExecResult:
|
||||
async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult:
|
||||
data = result.get("data", {})
|
||||
output = data.get("output", {})
|
||||
error = data.get("error", "")
|
||||
@@ -44,6 +45,9 @@ def handle_result(result: dict) -> ToolExecResult:
|
||||
type="image", data=img["image/png"], mimeType="image/png"
|
||||
)
|
||||
)
|
||||
|
||||
if event.get_platform_name() == "webchat":
|
||||
await event.send(message=MessageChain().base64_image(img["image/png"]))
|
||||
if text:
|
||||
resp.content.append(mcp.types.TextContent(type="text", text=text))
|
||||
|
||||
@@ -68,7 +72,7 @@ class PythonTool(FunctionTool):
|
||||
)
|
||||
try:
|
||||
result = await sb.python.exec(code, silent=silent)
|
||||
return handle_result(result)
|
||||
return await handle_result(result, context.context.event)
|
||||
except Exception as e:
|
||||
return f"Error executing code: {str(e)}"
|
||||
|
||||
@@ -89,6 +93,6 @@ class LocalPythonTool(FunctionTool):
|
||||
sb = get_local_booter()
|
||||
try:
|
||||
result = await sb.python.exec(code, silent=silent)
|
||||
return handle_result(result)
|
||||
return await handle_result(result, context.context.event)
|
||||
except Exception as e:
|
||||
return f"Error executing code: {str(e)}"
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.17.2"
|
||||
VERSION = "4.17.3"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
|
||||
@@ -25,10 +25,14 @@ import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from enum import Enum
|
||||
|
||||
from pydantic.v1 import BaseModel
|
||||
if sys.version_info >= (3, 14):
|
||||
from pydantic import BaseModel
|
||||
else:
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from astrbot.core import astrbot_config, file_token_service, logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
@@ -85,7 +89,7 @@ class BaseMessageComponent(BaseModel):
|
||||
|
||||
|
||||
class Plain(BaseMessageComponent):
|
||||
type = ComponentType.Plain
|
||||
type: ComponentType = ComponentType.Plain
|
||||
text: str
|
||||
convert: bool | None = True
|
||||
|
||||
@@ -100,7 +104,7 @@ class Plain(BaseMessageComponent):
|
||||
|
||||
|
||||
class Face(BaseMessageComponent):
|
||||
type = ComponentType.Face
|
||||
type: ComponentType = ComponentType.Face
|
||||
id: int
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
@@ -108,7 +112,7 @@ class Face(BaseMessageComponent):
|
||||
|
||||
|
||||
class Record(BaseMessageComponent):
|
||||
type = ComponentType.Record
|
||||
type: ComponentType = ComponentType.Record
|
||||
file: str | None = ""
|
||||
magic: bool | None = False
|
||||
url: str | None = ""
|
||||
@@ -215,7 +219,7 @@ class Record(BaseMessageComponent):
|
||||
|
||||
|
||||
class Video(BaseMessageComponent):
|
||||
type = ComponentType.Video
|
||||
type: ComponentType = ComponentType.Video
|
||||
file: str
|
||||
cover: str | None = ""
|
||||
c: int | None = 2
|
||||
@@ -301,7 +305,7 @@ class Video(BaseMessageComponent):
|
||||
|
||||
|
||||
class At(BaseMessageComponent):
|
||||
type = ComponentType.At
|
||||
type: ComponentType = ComponentType.At
|
||||
qq: int | str # 此处str为all时代表所有人
|
||||
name: str | None = ""
|
||||
|
||||
@@ -323,28 +327,28 @@ class AtAll(At):
|
||||
|
||||
|
||||
class RPS(BaseMessageComponent): # TODO
|
||||
type = ComponentType.RPS
|
||||
type: ComponentType = ComponentType.RPS
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
super().__init__(**_)
|
||||
|
||||
|
||||
class Dice(BaseMessageComponent): # TODO
|
||||
type = ComponentType.Dice
|
||||
type: ComponentType = ComponentType.Dice
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
super().__init__(**_)
|
||||
|
||||
|
||||
class Shake(BaseMessageComponent): # TODO
|
||||
type = ComponentType.Shake
|
||||
type: ComponentType = ComponentType.Shake
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
super().__init__(**_)
|
||||
|
||||
|
||||
class Share(BaseMessageComponent):
|
||||
type = ComponentType.Share
|
||||
type: ComponentType = ComponentType.Share
|
||||
url: str
|
||||
title: str
|
||||
content: str | None = ""
|
||||
@@ -355,7 +359,7 @@ class Share(BaseMessageComponent):
|
||||
|
||||
|
||||
class Contact(BaseMessageComponent): # TODO
|
||||
type = ComponentType.Contact
|
||||
type: ComponentType = ComponentType.Contact
|
||||
_type: str # type 字段冲突
|
||||
id: int | None = 0
|
||||
|
||||
@@ -364,7 +368,7 @@ class Contact(BaseMessageComponent): # TODO
|
||||
|
||||
|
||||
class Location(BaseMessageComponent): # TODO
|
||||
type = ComponentType.Location
|
||||
type: ComponentType = ComponentType.Location
|
||||
lat: float
|
||||
lon: float
|
||||
title: str | None = ""
|
||||
@@ -375,7 +379,7 @@ class Location(BaseMessageComponent): # TODO
|
||||
|
||||
|
||||
class Music(BaseMessageComponent):
|
||||
type = ComponentType.Music
|
||||
type: ComponentType = ComponentType.Music
|
||||
_type: str
|
||||
id: int | None = 0
|
||||
url: str | None = ""
|
||||
@@ -392,7 +396,7 @@ class Music(BaseMessageComponent):
|
||||
|
||||
|
||||
class Image(BaseMessageComponent):
|
||||
type = ComponentType.Image
|
||||
type: ComponentType = ComponentType.Image
|
||||
file: str | None = ""
|
||||
_type: str | None = ""
|
||||
subType: int | None = 0
|
||||
@@ -507,7 +511,7 @@ class Image(BaseMessageComponent):
|
||||
|
||||
|
||||
class Reply(BaseMessageComponent):
|
||||
type = ComponentType.Reply
|
||||
type: ComponentType = ComponentType.Reply
|
||||
id: str | int
|
||||
"""所引用的消息 ID"""
|
||||
chain: list["BaseMessageComponent"] | None = []
|
||||
@@ -543,7 +547,7 @@ class Poke(BaseMessageComponent):
|
||||
|
||||
|
||||
class Forward(BaseMessageComponent):
|
||||
type = ComponentType.Forward
|
||||
type: ComponentType = ComponentType.Forward
|
||||
id: str
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
@@ -553,7 +557,7 @@ class Forward(BaseMessageComponent):
|
||||
class Node(BaseMessageComponent):
|
||||
"""群合并转发消息"""
|
||||
|
||||
type = ComponentType.Node
|
||||
type: ComponentType = ComponentType.Node
|
||||
id: int | None = 0 # 忽略
|
||||
name: str | None = "" # qq昵称
|
||||
uin: str | None = "0" # qq号
|
||||
@@ -605,7 +609,7 @@ class Node(BaseMessageComponent):
|
||||
|
||||
|
||||
class Nodes(BaseMessageComponent):
|
||||
type = ComponentType.Nodes
|
||||
type: ComponentType = ComponentType.Nodes
|
||||
nodes: list[Node]
|
||||
|
||||
def __init__(self, nodes: list[Node], **_) -> None:
|
||||
@@ -631,7 +635,7 @@ class Nodes(BaseMessageComponent):
|
||||
|
||||
|
||||
class Json(BaseMessageComponent):
|
||||
type = ComponentType.Json
|
||||
type: ComponentType = ComponentType.Json
|
||||
data: dict
|
||||
|
||||
def __init__(self, data: str | dict, **_) -> None:
|
||||
@@ -641,14 +645,14 @@ class Json(BaseMessageComponent):
|
||||
|
||||
|
||||
class Unknown(BaseMessageComponent):
|
||||
type = ComponentType.Unknown
|
||||
type: ComponentType = ComponentType.Unknown
|
||||
text: str
|
||||
|
||||
|
||||
class File(BaseMessageComponent):
|
||||
"""文件消息段"""
|
||||
|
||||
type = ComponentType.File
|
||||
type: ComponentType = ComponentType.File
|
||||
name: str | None = "" # 名字
|
||||
file_: str | None = "" # 本地路径
|
||||
url: str | None = "" # url
|
||||
@@ -783,7 +787,7 @@ class File(BaseMessageComponent):
|
||||
|
||||
|
||||
class WechatEmoji(BaseMessageComponent):
|
||||
type = ComponentType.WechatEmoji
|
||||
type: ComponentType = ComponentType.WechatEmoji
|
||||
md5: str | None = ""
|
||||
md5_len: int | None = 0
|
||||
cdnurl: str | None = ""
|
||||
|
||||
@@ -22,7 +22,6 @@ from astrbot.core.utils.network_utils import (
|
||||
)
|
||||
|
||||
from ..register import register_provider_adapter
|
||||
from .default import with_model_request_retry
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
@@ -205,7 +204,6 @@ class ProviderAnthropic(Provider):
|
||||
if usage.output_tokens is not None:
|
||||
token_usage.output = usage.output_tokens
|
||||
|
||||
@with_model_request_retry()
|
||||
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
|
||||
if tools:
|
||||
if tool_list := tools.get_func_desc_anthropic_style():
|
||||
@@ -267,10 +265,6 @@ class ProviderAnthropic(Provider):
|
||||
|
||||
return llm_response
|
||||
|
||||
@with_model_request_retry()
|
||||
async def _create_message_stream(self, payloads: dict, extra_body: dict):
|
||||
return self.client.messages.stream(**payloads, extra_body=extra_body)
|
||||
|
||||
async def _query_stream(
|
||||
self,
|
||||
payloads: dict,
|
||||
@@ -299,8 +293,9 @@ class ProviderAnthropic(Provider):
|
||||
"type": "enabled",
|
||||
}
|
||||
|
||||
stream_ctx = await self._create_message_stream(payloads, extra_body)
|
||||
async with stream_ctx as stream:
|
||||
async with self.client.messages.stream(
|
||||
**payloads, extra_body=extra_body
|
||||
) as stream:
|
||||
assert isinstance(stream, anthropic.AsyncMessageStream)
|
||||
async for event in stream:
|
||||
if event.type == "message_start":
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
from tenacity import (
|
||||
AsyncRetrying,
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
MODEL_REQUEST_RETRY_ATTEMPTS = 5
|
||||
MODEL_REQUEST_RETRY_WAIT_MAX_SECONDS = 15
|
||||
MODEL_REQUEST_RETRY_WAIT_MIN_SECONDS = 1
|
||||
MODEL_REQUEST_RETRY_WAIT_MULTIPLIER = 1
|
||||
|
||||
|
||||
def with_model_request_retry():
|
||||
return retry(
|
||||
retry=retry_if_exception_type(Exception),
|
||||
stop=stop_after_attempt(MODEL_REQUEST_RETRY_ATTEMPTS),
|
||||
wait=wait_exponential(
|
||||
multiplier=MODEL_REQUEST_RETRY_WAIT_MULTIPLIER,
|
||||
min=MODEL_REQUEST_RETRY_WAIT_MIN_SECONDS,
|
||||
max=MODEL_REQUEST_RETRY_WAIT_MAX_SECONDS,
|
||||
),
|
||||
reraise=True,
|
||||
)
|
||||
|
||||
|
||||
def get_model_request_async_retrying() -> AsyncRetrying:
|
||||
return AsyncRetrying(
|
||||
retry=retry_if_exception_type(Exception),
|
||||
stop=stop_after_attempt(MODEL_REQUEST_RETRY_ATTEMPTS),
|
||||
wait=wait_exponential(
|
||||
multiplier=MODEL_REQUEST_RETRY_WAIT_MULTIPLIER,
|
||||
min=MODEL_REQUEST_RETRY_WAIT_MIN_SECONDS,
|
||||
max=MODEL_REQUEST_RETRY_WAIT_MAX_SECONDS,
|
||||
),
|
||||
reraise=True,
|
||||
)
|
||||
@@ -21,7 +21,6 @@ from astrbot.core.utils.io import download_image_by_url
|
||||
from astrbot.core.utils.network_utils import is_connection_error, log_connection_failure
|
||||
|
||||
from ..register import register_provider_adapter
|
||||
from .default import get_model_request_async_retrying, with_model_request_retry
|
||||
|
||||
|
||||
class SuppressNonTextPartsWarning(logging.Filter):
|
||||
@@ -514,7 +513,6 @@ class ProviderGoogleGenAI(Provider):
|
||||
llm_response.reasoning_signature = base64.b64encode(ts).decode("utf-8")
|
||||
return MessageChain(chain=chain)
|
||||
|
||||
@with_model_request_retry()
|
||||
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
|
||||
"""非流式请求 Gemini API"""
|
||||
system_instruction = next(
|
||||
@@ -603,17 +601,6 @@ class ProviderGoogleGenAI(Provider):
|
||||
self,
|
||||
payloads: dict,
|
||||
tools: ToolSet | None,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
async for attempt in get_model_request_async_retrying():
|
||||
with attempt:
|
||||
async for response in self._query_stream_once(payloads, tools):
|
||||
yield response
|
||||
return
|
||||
|
||||
async def _query_stream_once(
|
||||
self,
|
||||
payloads: dict,
|
||||
tools: ToolSet | None,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
"""流式请求 Gemini API"""
|
||||
system_instruction = next(
|
||||
@@ -772,7 +759,18 @@ class ProviderGoogleGenAI(Provider):
|
||||
|
||||
payloads = {"messages": context_query, "model": model}
|
||||
|
||||
return await self._query(payloads, func_tool)
|
||||
retry = 10
|
||||
keys = self.api_keys.copy()
|
||||
|
||||
for _ in range(retry):
|
||||
try:
|
||||
return await self._query(payloads, func_tool)
|
||||
except APIError as e:
|
||||
if await self._handle_api_error(e, keys):
|
||||
continue
|
||||
break
|
||||
|
||||
raise Exception("请求失败。")
|
||||
|
||||
async def text_chat_stream(
|
||||
self,
|
||||
@@ -816,8 +814,18 @@ class ProviderGoogleGenAI(Provider):
|
||||
|
||||
payloads = {"messages": context_query, "model": model}
|
||||
|
||||
async for response in self._query_stream(payloads, func_tool):
|
||||
yield response
|
||||
retry = 10
|
||||
keys = self.api_keys.copy()
|
||||
|
||||
for _ in range(retry):
|
||||
try:
|
||||
async for response in self._query_stream(payloads, func_tool):
|
||||
yield response
|
||||
break
|
||||
except APIError as e:
|
||||
if await self._handle_api_error(e, keys):
|
||||
continue
|
||||
break
|
||||
|
||||
async def get_models(self):
|
||||
try:
|
||||
|
||||
@@ -31,7 +31,6 @@ from astrbot.core.utils.network_utils import (
|
||||
from astrbot.core.utils.string_utils import normalize_and_dedupe_strings
|
||||
|
||||
from ..register import register_provider_adapter
|
||||
from .default import get_model_request_async_retrying, with_model_request_retry
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
@@ -222,7 +221,6 @@ class ProviderOpenAIOfficial(Provider):
|
||||
except NotFoundError as e:
|
||||
raise Exception(f"获取模型列表失败:{e}")
|
||||
|
||||
@with_model_request_retry()
|
||||
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
|
||||
if tools:
|
||||
model = payloads.get("model", "").lower()
|
||||
@@ -248,6 +246,8 @@ class ProviderOpenAIOfficial(Provider):
|
||||
if isinstance(custom_extra_body, dict):
|
||||
extra_body.update(custom_extra_body)
|
||||
|
||||
model = payloads.get("model", "").lower()
|
||||
|
||||
completion = await self.client.chat.completions.create(
|
||||
**payloads,
|
||||
stream=False,
|
||||
@@ -269,17 +269,6 @@ class ProviderOpenAIOfficial(Provider):
|
||||
self,
|
||||
payloads: dict,
|
||||
tools: ToolSet | None,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
async for attempt in get_model_request_async_retrying():
|
||||
with attempt:
|
||||
async for response in self._query_stream_once(payloads, tools):
|
||||
yield response
|
||||
return
|
||||
|
||||
async def _query_stream_once(
|
||||
self,
|
||||
payloads: dict,
|
||||
tools: ToolSet | None,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
"""流式查询API,逐步返回结果"""
|
||||
if tools:
|
||||
@@ -727,7 +716,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
extra_user_content_parts=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
payloads, _ = await self._prepare_chat_payload(
|
||||
payloads, context_query = await self._prepare_chat_payload(
|
||||
prompt,
|
||||
image_urls,
|
||||
contexts,
|
||||
@@ -739,9 +728,47 @@ class ProviderOpenAIOfficial(Provider):
|
||||
)
|
||||
|
||||
llm_response = None
|
||||
if self.api_keys:
|
||||
self.client.api_key = random.choice(self.api_keys)
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
max_retries = 10
|
||||
available_api_keys = self.api_keys.copy()
|
||||
chosen_key = random.choice(available_api_keys)
|
||||
image_fallback_used = False
|
||||
|
||||
last_exception = None
|
||||
retry_cnt = 0
|
||||
for retry_cnt in range(max_retries):
|
||||
try:
|
||||
self.client.api_key = chosen_key
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
break
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
(
|
||||
success,
|
||||
chosen_key,
|
||||
available_api_keys,
|
||||
payloads,
|
||||
context_query,
|
||||
func_tool,
|
||||
image_fallback_used,
|
||||
) = await self._handle_api_error(
|
||||
e,
|
||||
payloads,
|
||||
context_query,
|
||||
func_tool,
|
||||
chosen_key,
|
||||
available_api_keys,
|
||||
retry_cnt,
|
||||
max_retries,
|
||||
image_fallback_used=image_fallback_used,
|
||||
)
|
||||
if success:
|
||||
break
|
||||
|
||||
if retry_cnt == max_retries - 1 or llm_response is None:
|
||||
logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。")
|
||||
if last_exception is None:
|
||||
raise Exception("未知错误")
|
||||
raise last_exception
|
||||
return llm_response
|
||||
|
||||
async def text_chat_stream(
|
||||
@@ -757,7 +784,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
"""流式对话,与服务商交互并逐步返回结果"""
|
||||
payloads, _ = await self._prepare_chat_payload(
|
||||
payloads, context_query = await self._prepare_chat_payload(
|
||||
prompt,
|
||||
image_urls,
|
||||
contexts,
|
||||
@@ -767,10 +794,48 @@ class ProviderOpenAIOfficial(Provider):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if self.api_keys:
|
||||
self.client.api_key = random.choice(self.api_keys)
|
||||
async for response in self._query_stream(payloads, func_tool):
|
||||
yield response
|
||||
max_retries = 10
|
||||
available_api_keys = self.api_keys.copy()
|
||||
chosen_key = random.choice(available_api_keys)
|
||||
image_fallback_used = False
|
||||
|
||||
last_exception = None
|
||||
retry_cnt = 0
|
||||
for retry_cnt in range(max_retries):
|
||||
try:
|
||||
self.client.api_key = chosen_key
|
||||
async for response in self._query_stream(payloads, func_tool):
|
||||
yield response
|
||||
break
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
(
|
||||
success,
|
||||
chosen_key,
|
||||
available_api_keys,
|
||||
payloads,
|
||||
context_query,
|
||||
func_tool,
|
||||
image_fallback_used,
|
||||
) = await self._handle_api_error(
|
||||
e,
|
||||
payloads,
|
||||
context_query,
|
||||
func_tool,
|
||||
chosen_key,
|
||||
available_api_keys,
|
||||
retry_cnt,
|
||||
max_retries,
|
||||
image_fallback_used=image_fallback_used,
|
||||
)
|
||||
if success:
|
||||
break
|
||||
|
||||
if retry_cnt == max_retries - 1:
|
||||
logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。")
|
||||
if last_exception is None:
|
||||
raise Exception("未知错误")
|
||||
raise last_exception
|
||||
|
||||
async def _remove_image_from_context(self, contexts: list):
|
||||
"""从上下文中删除所有带有 image 的记录"""
|
||||
|
||||
@@ -513,6 +513,16 @@ class PluginManager:
|
||||
)
|
||||
logger.info(metadata)
|
||||
metadata.config = plugin_config
|
||||
p_name = (metadata.name or "unknown").lower().replace("/", "_")
|
||||
p_author = (metadata.author or "unknown").lower().replace("/", "_")
|
||||
plugin_id = f"{p_author}/{p_name}"
|
||||
|
||||
# 在实例化前注入类属性,保证插件 __init__ 可读取这些值
|
||||
if metadata.star_cls_type:
|
||||
setattr(metadata.star_cls_type, "name", p_name)
|
||||
setattr(metadata.star_cls_type, "author", p_author)
|
||||
setattr(metadata.star_cls_type, "plugin_id", plugin_id)
|
||||
|
||||
if path not in inactivated_plugins:
|
||||
# 只有没有禁用插件时才实例化插件类
|
||||
if plugin_config and metadata.star_cls_type:
|
||||
@@ -530,17 +540,10 @@ class PluginManager:
|
||||
context=self.context,
|
||||
)
|
||||
|
||||
p_name = (metadata.name or "unknown").lower().replace("/", "_")
|
||||
p_author = (
|
||||
(metadata.author or "unknown").lower().replace("/", "_")
|
||||
)
|
||||
setattr(metadata.star_cls, "name", p_name)
|
||||
setattr(metadata.star_cls, "author", p_author)
|
||||
setattr(
|
||||
metadata.star_cls,
|
||||
"plugin_id",
|
||||
f"{p_author}/{p_name}",
|
||||
)
|
||||
if metadata.star_cls:
|
||||
setattr(metadata.star_cls, "name", p_name)
|
||||
setattr(metadata.star_cls, "author", p_author)
|
||||
setattr(metadata.star_cls, "plugin_id", plugin_id)
|
||||
else:
|
||||
logger.info(f"插件 {metadata.name} 已被禁用。")
|
||||
|
||||
|
||||
@@ -148,8 +148,8 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
update_data = await self.fetch_release_info(self.ASTRBOT_RELEASE_API, latest)
|
||||
file_url = None
|
||||
|
||||
if os.environ.get("ASTRBOT_CLI"):
|
||||
raise Exception("不支持更新CLI启动的AstrBot") # 避免版本管理混乱
|
||||
if os.environ.get("ASTRBOT_CLI") or os.environ.get("ASTRBOT_LAUNCHER"):
|
||||
raise Exception("不支持更新此方式启动的AstrBot") # 避免版本管理混乱
|
||||
|
||||
if latest:
|
||||
latest_version = update_data[0]["tag_name"]
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
## What's Changed
|
||||
|
||||
### 修复
|
||||
- ‼️ 修复 Python 3.14 环境下 `'Plain' object has no attribute 'text'` 报错问题 ([#5154](https://github.com/AstrBotDevs/AstrBot/issues/5154))。
|
||||
- ‼️ 修复插件元数据处理流程:在实例化前注入必要属性,避免初始化阶段元数据缺失 ([#5155](https://github.com/AstrBotDevs/AstrBot/issues/5155))。
|
||||
- 修复桌面端后端构建中 AstrBot 内置插件运行时依赖未打包的问题 ([#5146](https://github.com/AstrBotDevs/AstrBot/issues/5146))。
|
||||
- 修复通过 AstrBot Launcher 启动时仍被检测并触发更新的问题。
|
||||
|
||||
### 优化
|
||||
|
||||
- Webchat 下,使用 `astrbot_execute_ipython` 工具如果返回了图片,会自动将图片发送到聊天中。
|
||||
|
||||
### 其他
|
||||
- 执行 `ruff format` 代码格式整理。
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### Fixes
|
||||
- ‼️ Fixed plugin metadata handling by injecting required attributes before instantiation to avoid missing metadata during initialization ([#5155](https://github.com/AstrBotDevs/AstrBot/issues/5155)).
|
||||
- ‼️ Fixed `'Plain' object has no attribute 'text'` error when using Python 3.14 ([#5154](https://github.com/AstrBotDevs/AstrBot/issues/5154)).
|
||||
- Fixed missing runtime dependencies for built-in plugins in desktop backend builds ([#5146](https://github.com/AstrBotDevs/AstrBot/issues/5146)).
|
||||
- Fixed update checks being triggered when AstrBot is launched via AstrBot Launcher.
|
||||
|
||||
### Improvements
|
||||
- In Webchat, when using the `astrbot_execute_ipython` tool, if an image is returned, it will automatically be sent to the chat.
|
||||
### Others
|
||||
- Applied `ruff format` code formatting.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "astrbot-desktop",
|
||||
"version": "4.17.2",
|
||||
"version": "4.17.3",
|
||||
"description": "AstrBot desktop wrapper",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
@@ -35,6 +35,16 @@ const args = [
|
||||
'aiosqlite',
|
||||
'--collect-all',
|
||||
'pip',
|
||||
'--collect-all',
|
||||
'bs4',
|
||||
'--collect-all',
|
||||
'readability',
|
||||
'--collect-all',
|
||||
'lxml',
|
||||
'--collect-all',
|
||||
'lxml_html_clean',
|
||||
'--collect-all',
|
||||
'rfc3987_syntax',
|
||||
'--collect-submodules',
|
||||
'astrbot.api',
|
||||
'--collect-submodules',
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.17.2"
|
||||
version = "4.17.3"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
Reference in New Issue
Block a user