fix: resolve type warnings across core modules

This commit is contained in:
LIghtJUNction
2026-05-21 22:45:44 +08:00
parent fb4f76af63
commit 3f4f171530
14 changed files with 326 additions and 100 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
from typing import Any, Generic
from typing import Any, Generic, cast
from pydantic import Field
from pydantic.dataclasses import dataclass
@@ -13,7 +13,7 @@ TContext = TypeVar("TContext", default=Any)
class ContextWrapper(Generic[TContext]):
"""A context for running an agent, which can be used to pass additional data or state."""
context: TContext | None = None
context: TContext = cast(TContext, None)
messages: list[Message] = Field(default_factory=list)
"""This field stores the llm message context for the agent run, agent runners will maintain this field automatically."""
tool_call_timeout: int = 120 # Default tool call timeout in seconds
@@ -1,5 +1,6 @@
import asyncio
import copy
import os
import time
import traceback
import typing as T
@@ -372,6 +373,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
overflow_dir = await anyio.Path(
self.tool_result_overflow_dir,
).resolve(strict=False)
overflow_dir_path = os.fspath(overflow_dir)
safe_tool_call_id = (
"".join(
ch if ch.isalnum() or ch in {"-", "_", "."} else "_"
@@ -380,12 +382,13 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
or "tool_call"
)
file_name = f"{safe_tool_call_id}_{uuid.uuid4().hex[:8]}.txt"
overflow_path = overflow_dir / file_name
overflow_path = os.path.join(overflow_dir_path, file_name)
def _run() -> str:
overflow_dir.mkdir(parents=True, exist_ok=True)
overflow_path.write_text(content, encoding="utf-8")
return str(overflow_path)
os.makedirs(overflow_dir_path, exist_ok=True)
with open(overflow_path, "w", encoding="utf-8") as f:
f.write(content)
return overflow_path
return await asyncio.to_thread(_run)
@@ -965,6 +968,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
) -> T.AsyncGenerator[_HandleFunctionToolsResult, None]:
"""处理函数工具调用。"""
tool_call_result_blocks: list[ToolCallMessageSegment] = []
last_func_tool_name = "unknown"
last_func_tool_id = "unknown"
logger.info(f"Agent 使用工具: {llm_response.tools_call_name}")
def _append_tool_call_result(tool_call_id: str, content: str) -> None:
@@ -983,6 +988,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
llm_response.tools_call_ids,
strict=False,
):
last_func_tool_name = func_tool_name
last_func_tool_id = func_tool_id
tool_call_streak = self._track_tool_call_streak(func_tool_name)
yield _HandleFunctionToolsResult.from_message_chain(
MessageChain(
@@ -1212,7 +1219,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
chain=[
Json(
data={
"id": func_tool_id,
"id": last_func_tool_id,
"ts": time.time(),
"result": last_tcr_content,
},
@@ -1220,7 +1227,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
],
),
)
logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}")
logger.info(f"Tool `{last_func_tool_name}` Result: {last_tcr_content}")
# 处理函数调用响应
if tool_call_result_blocks:
@@ -1237,7 +1244,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
contexts: list[dict[str, T.Any]] = []
for msg in self.run_context.messages:
if hasattr(msg, "model_dump"):
contexts.append(msg.model_dump()) # type: ignore[call-arg]
contexts.append(msg.model_dump())
elif isinstance(msg, dict):
contexts.append(copy.deepcopy(msg))
instruction = self.SKILLS_LIKE_REQUERY_INSTRUCTION_TEMPLATE.format(
+1 -4
View File
@@ -55,10 +55,7 @@ class ComputerBooter(abc.ABC):
async def shutdown(self, **kwargs) -> None:
"""Shut down the computer sandbox.
Subclasses may accept extra keyword arguments for
type-specific cleanup (e.g. ``delete_sandbox`` for
ShipyardNeoBooter). The default implementation ignores
them.
Subclasses may accept type-specific keyword arguments.
"""
async def upload_file(self, path: str, file_name: str) -> dict:
+18 -11
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio
import functools
import random
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
import aiohttp
import anyio
@@ -15,7 +15,7 @@ from shipyard.shell import ShellComponent as ShipyardShellComponent
from astrbot.api import logger
if TYPE_CHECKING:
from astrbot.core.agent.tool import FunctionTool, ToolSchema
from astrbot.core.agent.tool import ToolSchema
from astrbot.core.computer.olayer import (
FileSystemComponent,
@@ -24,6 +24,7 @@ from astrbot.core.computer.olayer import (
)
from .base import ComputerBooter
from .shipyard import ShipyardFileSystemWrapper, ShipyardShellWrapper
class MockShipyardSandboxClient:
@@ -176,7 +177,8 @@ class BoxliteBooter(ComputerBooter):
session_id,
)
random_port = random.randint(20000, 30000)
self.box = boxlite.SimpleBox( # type: ignore
SimpleBox = vars(boxlite)["SimpleBox"]
self.box = SimpleBox(
image="soulter/shipyard-ship",
memory_mib=512,
cpus=1,
@@ -196,25 +198,27 @@ class BoxliteBooter(ComputerBooter):
self.mocked = MockShipyardSandboxClient(
sb_url=f"http://127.0.0.1:{random_port}",
)
self._fs = ShipyardFileSystemComponent(
client=self.mocked,
raw_fs = ShipyardFileSystemComponent(
client=cast(Any, self.mocked),
ship_id=self.box.id,
session_id=session_id,
)
self._python = ShipyardPythonComponent(
client=self.mocked,
client=cast(Any, self.mocked),
ship_id=self.box.id,
session_id=session_id,
)
self._shell = ShipyardShellComponent(
client=self.mocked,
raw_shell = ShipyardShellComponent(
client=cast(Any, self.mocked),
ship_id=self.box.id,
session_id=session_id,
)
self._shell = ShipyardShellWrapper(cast(Any, raw_shell))
self._fs = ShipyardFileSystemWrapper(cast(Any, raw_fs), self._shell)
await self.mocked.wait_healthy(self.box.id, session_id)
async def shutdown(self) -> None:
async def shutdown(self, **kwargs) -> None:
logger.info(
"[Computer] booter_shutdown booter=boxlite ship_id=%s status=starting",
self.box.id,
@@ -225,6 +229,9 @@ class BoxliteBooter(ComputerBooter):
self.box.id,
)
async def available(self) -> bool:
return hasattr(self, "box")
@property
def fs(self) -> FileSystemComponent:
return self._fs
@@ -243,7 +250,7 @@ class BoxliteBooter(ComputerBooter):
@classmethod
@functools.cache
def _default_tools(cls) -> tuple[FunctionTool, ...]:
def _default_tools(cls) -> tuple[ToolSchema, ...]:
from astrbot.core.computer.tools import (
ExecuteShellTool,
FileDownloadTool,
@@ -251,7 +258,7 @@ class BoxliteBooter(ComputerBooter):
PythonTool,
)
return ( # type: ignore
return (
ExecuteShellTool(),
PythonTool(),
FileUploadTool(),
+12 -2
View File
@@ -122,7 +122,10 @@ class BwrapShellComponent(ShellComponent):
timeout: int | None = 30,
shell: bool = True,
background: bool = False,
session_id: str | None = None,
) -> dict[str, Any]:
_ = session_id
def _run() -> dict[str, Any]:
run_env = os.environ.copy()
if env:
@@ -235,7 +238,14 @@ class HostBackedFileSystemComponent(FileSystemComponent):
await asyncio.to_thread(os.chmod, p, mode)
return {"success": True, "path": p}
async def read_file(self, path: str, encoding: str = "utf-8") -> dict[str, Any]:
async def read_file(
self,
path: str,
encoding: str = "utf-8",
offset: int | None = None,
limit: int | None = None,
) -> dict[str, Any]:
_ = offset, limit
p = self._safe_path(path)
try:
content = await asyncio.to_thread(_read_file_sync, p, encoding)
@@ -400,7 +410,7 @@ class BwrapBooter(ComputerBooter):
{}""".format(test_py["stderr"]),
)
async def shutdown(self) -> None:
async def shutdown(self, **kwargs) -> None:
config = self.config
if config is None:
return
+91 -21
View File
@@ -2,8 +2,10 @@ from __future__ import annotations
import asyncio
import base64
import importlib
import inspect
import shlex
from collections.abc import Callable
from dataclasses import asdict, dataclass, is_dataclass
from pathlib import Path
from typing import Any
@@ -85,12 +87,14 @@ def _maybe_model_dump(value: Any) -> dict[str, Any]:
return value
if is_dataclass(value) and not isinstance(value, type):
return asdict(value)
if hasattr(value, "model_dump"):
dumped = value.model_dump()
model_dump = getattr(value, "model_dump", None)
if callable(model_dump):
dumped = model_dump()
if isinstance(dumped, dict):
return dumped
if hasattr(value, "dict"):
dumped = value.dict()
dict_method = getattr(value, "dict", None)
if callable(dict_method):
dumped = dict_method()
if isinstance(dumped, dict):
return dumped
attr_payload = {
@@ -139,13 +143,15 @@ def _normalize_process_result(raw: Any) -> ProcessResult:
stdout = first_text("stdout", "output")
stderr = first_text("stderr", "error")
exit_code = payload.get("exit_code")
if exit_code is None:
exit_code = payload.get("returncode")
if exit_code is None:
exit_code = payload.get("return_code")
if exit_code is None:
raw_exit_code = payload.get("exit_code")
if raw_exit_code is None:
raw_exit_code = payload.get("returncode")
if raw_exit_code is None:
raw_exit_code = payload.get("return_code")
if raw_exit_code is None:
exit_code = 0 if not stderr else 1
else:
exit_code = int(raw_exit_code)
success = bool(payload.get("success", not stderr and exit_code in (0, None)))
return ProcessResult(
stdout=stdout,
@@ -284,9 +290,10 @@ class CuaShellComponent(ShellComponent):
self._sandbox = sandbox
self._os_type = os_type.lower()
shell = sandbox.shell
self._exec_raw = getattr(shell, "exec", None) or getattr(shell, "run", None)
if self._exec_raw is None:
exec_raw = getattr(shell, "exec", None) or getattr(shell, "run", None)
if exec_raw is None:
raise RuntimeError("CUA sandbox shell must provide `.exec` or `.run`.")
self._exec_raw: Callable[..., Any] = exec_raw
async def exec(
self,
@@ -296,7 +303,9 @@ class CuaShellComponent(ShellComponent):
timeout: int | None = 30,
shell: bool = True,
background: bool = False,
session_id: str | None = None,
) -> dict[str, Any]:
_ = session_id
if not shell:
return {
"stdout": "",
@@ -404,11 +413,19 @@ def _write_result(path: str, result: dict[str, Any]) -> dict[str, Any]:
return {"success": True, "path": path, **result}
CUA_DEFAULT_IMAGE = str(CUA_DEFAULT_CONFIG["image"])
CUA_DEFAULT_OS_TYPE = str(CUA_DEFAULT_CONFIG["os_type"])
CUA_DEFAULT_TTL = int(CUA_DEFAULT_CONFIG["ttl"])
CUA_DEFAULT_TELEMETRY_ENABLED = bool(CUA_DEFAULT_CONFIG["telemetry_enabled"])
CUA_DEFAULT_LOCAL = bool(CUA_DEFAULT_CONFIG["local"])
CUA_DEFAULT_API_KEY = str(CUA_DEFAULT_CONFIG["api_key"])
class CuaFileSystemComponent(FileSystemComponent):
def __init__(
self,
sandbox: Any,
os_type: str = CUA_DEFAULT_CONFIG["os_type"],
os_type: str = CUA_DEFAULT_OS_TYPE,
) -> None:
self._shell = CuaShellComponent(sandbox, os_type=os_type)
self._fs_components = _resolve_files_components(sandbox)
@@ -546,6 +563,26 @@ class _PosixShellFileSystem(FileSystemComponent):
return None
return _non_posix_filesystem_result(path, self._os_type)
async def create_file(
self,
path: str,
content: str = "",
mode: int = 0o644,
) -> dict[str, Any]:
write_result = await self.write_file(path, content)
if not write_result.get("success"):
return {**write_result, "mode": mode, "mode_applied": False}
chmod_result = await self._shell.exec(f"chmod {mode:o} {shlex.quote(path)}")
if chmod_result.get("stderr"):
return {
"success": True,
"path": path,
"mode": mode,
"mode_applied": False,
"mode_error": chmod_result["stderr"],
}
return {"success": True, "path": path, "mode": mode, "mode_applied": True}
async def read_file(
self,
path: str,
@@ -623,6 +660,36 @@ class _PosixShellFileSystem(FileSystemComponent):
before_context=before_context,
)
async def edit_file(
self,
path: str,
old_string: str,
new_string: str,
replace_all: bool = False,
encoding: str = "utf-8",
) -> dict[str, Any]:
read_result = await self.read_file(path, encoding=encoding)
if not read_result.get("success"):
return read_result
content = str(read_result.get("content", ""))
occurrences = content.count(old_string)
if occurrences == 0:
return {
"success": False,
"path": path,
"error": "old string not found in file",
"replacements": 0,
}
updated = content.replace(old_string, new_string, -1 if replace_all else 1)
write_result = await self.write_file(path, updated, encoding=encoding)
if not write_result.get("success"):
return write_result
return {
"success": True,
"path": path,
"replacements": occurrences if replace_all else 1,
}
async def _list_dir_via_shell(
shell: CuaShellComponent,
@@ -734,12 +801,12 @@ class _CuaRuntime:
class CuaBooter(ComputerBooter):
def __init__(
self,
image: str = CUA_DEFAULT_CONFIG["image"],
os_type: str = CUA_DEFAULT_CONFIG["os_type"],
ttl: int = CUA_DEFAULT_CONFIG["ttl"],
telemetry_enabled: bool = CUA_DEFAULT_CONFIG["telemetry_enabled"],
local: bool = CUA_DEFAULT_CONFIG["local"],
api_key: str = CUA_DEFAULT_CONFIG["api_key"],
image: str = CUA_DEFAULT_IMAGE,
os_type: str = CUA_DEFAULT_OS_TYPE,
ttl: int = CUA_DEFAULT_TTL,
telemetry_enabled: bool = CUA_DEFAULT_TELEMETRY_ENABLED,
local: bool = CUA_DEFAULT_LOCAL,
api_key: str = CUA_DEFAULT_API_KEY,
) -> None:
self.image = image
self.os_type = os_type
@@ -752,13 +819,16 @@ class CuaBooter(ComputerBooter):
async def boot(self, session_id: str) -> None:
_ = session_id
try:
from cua import Image, Sandbox
cua_module = importlib.import_module("cua")
except ImportError as exc:
raise RuntimeError(
"CUA sandbox support requires the optional `cua` package. "
"Install it with `pip install cua` in the AstrBot environment.",
) from exc
Image = vars(cua_module)["Image"]
Sandbox = vars(cua_module)["Sandbox"]
image_obj = self._build_image(Image)
ephemeral_kwargs = self._build_ephemeral_kwargs(Sandbox.ephemeral)
sandbox_cm = Sandbox.ephemeral(image_obj, **ephemeral_kwargs)
@@ -808,7 +878,7 @@ class CuaBooter(ComputerBooter):
kwargs["api_key"] = self.api_key
return kwargs
async def shutdown(self) -> None:
async def shutdown(self, **kwargs) -> None:
if self._runtime is not None:
await self._runtime.sandbox_cm.__aexit__(None, None, None)
self._runtime = None
+1 -1
View File
@@ -333,7 +333,7 @@ class LocalBooter(ComputerBooter):
async def boot(self, session_id: str) -> None:
logger.info(f"Local computer booter initialized for session: {session_id}")
async def shutdown(self) -> None:
async def shutdown(self, **kwargs) -> None:
await LocalShellComponent.shutdown_all()
logger.info("Local computer booter shutdown complete.")
+4 -2
View File
@@ -44,7 +44,9 @@ class ShipyardShellWrapper:
timeout: int | None = 300,
shell: bool = True,
background: bool = False,
session_id: str | None = None,
) -> dict[str, Any]:
_ = session_id
if not shell:
return {
"stdout": "",
@@ -246,7 +248,7 @@ class ShipyardBooter(ComputerBooter):
self._shell = ShipyardShellWrapper(self._ship.shell) # type: ignore[arg-type]
self._fs = ShipyardFileSystemWrapper(self._ship.fs, self._shell) # type: ignore[arg-type]
async def shutdown(self) -> None:
async def shutdown(self, **kwargs) -> None:
logger.info("[Computer] booter_shutdown booter=shipyard status=done")
@property
@@ -259,7 +261,7 @@ class ShipyardBooter(ComputerBooter):
@property
def shell(self) -> ShellComponent:
return self._shell # type: ignore[return-value]
return self._shell
async def upload_file(self, path: str, file_name: str) -> dict:
"""Upload file to sandbox"""
+73 -6
View File
@@ -21,6 +21,7 @@ from astrbot.core.computer.olayer import (
)
from .shell_background import build_detached_shell_command
from .shipyard_search_file_util import search_files_via_shell
try:
from shipyard_neo import BayClient # noqa: F401
@@ -91,7 +92,9 @@ class NeoShellComponent(ShellComponent):
timeout: int | None = 300, # noqa: ASYNC109
shell: bool = True,
background: bool = False,
session_id: str | None = None,
) -> dict[str, Any]:
_ = session_id
if not shell:
return {
"stdout": "",
@@ -163,10 +166,22 @@ class NeoFileSystemComponent(FileSystemComponent):
await self._sandbox.filesystem.write_file(path, content)
return {"success": True, "path": path}
async def read_file(self, path: str, encoding: str = "utf-8") -> dict[str, Any]:
async def read_file(
self,
path: str,
encoding: str = "utf-8",
offset: int | None = None,
limit: int | None = None,
) -> dict[str, Any]:
_ = encoding
content = await self._sandbox.filesystem.read_file(path)
return {"success": True, "path": path, "content": content}
text = str(content)
if offset is not None or limit is not None:
lines = text.splitlines(keepends=True)
start = 0 if offset is None else offset
selected = lines[start:] if limit is None else lines[start : start + limit]
text = "".join(selected)
return {"success": True, "path": path, "content": text}
async def write_file(
self,
@@ -198,6 +213,57 @@ class NeoFileSystemComponent(FileSystemComponent):
data.append(item)
return {"success": True, "path": path, "entries": data}
async def search_files(
self,
pattern: str,
path: str | None = None,
glob: str | None = None,
after_context: int | None = None,
before_context: int | None = None,
) -> dict[str, Any]:
if self._shell is None:
raise RuntimeError(
"NeoFileSystemComponent requires a shell for search_files."
)
return await search_files_via_shell(
self._shell,
pattern=pattern,
path=path,
glob=glob,
after_context=after_context,
before_context=before_context,
)
async def edit_file(
self,
path: str,
old_string: str,
new_string: str,
replace_all: bool = False,
encoding: str = "utf-8",
) -> dict[str, Any]:
read_result = await self.read_file(path, encoding=encoding)
if not read_result.get("success"):
return read_result
content = str(read_result.get("content", ""))
occurrences = content.count(old_string)
if occurrences == 0:
return {
"success": False,
"path": path,
"error": "old string not found in file",
"replacements": 0,
}
updated = content.replace(old_string, new_string, -1 if replace_all else 1)
write_result = await self.write_file(path, updated, encoding=encoding)
if not write_result.get("success"):
return write_result
return {
"success": True,
"path": path,
"replacements": occurrences if replace_all else 1,
}
class NeoBrowserComponent(BrowserComponent):
def __init__(self, sandbox: Any) -> None:
@@ -500,7 +566,8 @@ class ShipyardNeoBooter(ComputerBooter):
)
return chosen
async def shutdown(self, *, delete_sandbox: bool = False) -> None:
async def shutdown(self, **kwargs) -> None:
delete_sandbox = bool(kwargs.get("delete_sandbox", False))
if self._client is not None:
sandbox_id = getattr(self._sandbox, "id", "unknown")
@@ -622,7 +689,7 @@ class ShipyardNeoBooter(ComputerBooter):
@classmethod
@functools.cache
def _base_tools(cls):
def _base_tools(cls) -> tuple[ToolSchema, ...]:
"""4 base + 11 Neo lifecycle = 15 tools (all Neo profiles)."""
from astrbot.core.computer.tools import (
AnnotateExecutionTool,
@@ -662,7 +729,7 @@ class ShipyardNeoBooter(ComputerBooter):
@classmethod
@functools.cache
def _browser_tools(cls):
def _browser_tools(cls) -> tuple[ToolSchema, ...]:
from astrbot.core.computer.tools import (
BrowserBatchExecTool,
BrowserExecTool,
@@ -681,7 +748,7 @@ class ShipyardNeoBooter(ComputerBooter):
caps = self.capabilities
if caps is None:
return self.__class__.get_default_tools()
tools = list(self._base_tools())
tools: list[ToolSchema] = list(self._base_tools())
if "browser" in caps:
tools.extend(self._browser_tools())
return tools
+16 -1
View File
@@ -5,7 +5,7 @@ from typing import Any
from astrbot.core.message.components import Plain
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageMember
from astrbot.core.platform.astrbot_message import AstrBotMessage, Group, MessageMember
from astrbot.core.platform.message_session import MessageSession
from astrbot.core.platform.message_type import MessageType
from astrbot.core.platform.platform_metadata import PlatformMetadata
@@ -63,5 +63,20 @@ class CronMessageEvent(AstrMessageEvent):
async for chain in generator:
await self.send(chain)
async def send_typing(self) -> None:
return None
async def stop_typing(self) -> None:
return None
async def _pre_send(self) -> None:
return None
async def _post_send(self) -> None:
return None
async def get_group(self, group_id: str | None = None, **kwargs) -> Group | None:
return None
__all__ = ["CronMessageEvent"]
-3
View File
@@ -336,8 +336,6 @@ class CronJobManager:
cron_event.role = "admin" if sender_id in admin_ids else "member"
if cron_payload.get("origin", "tool") == "api":
cron_event.role = "admin"
from astrbot.core.computer.computer_tool_provider import ComputerToolProvider
tool_call_timeout = cfg.get("provider_settings", {}).get(
"tool_call_timeout",
120,
@@ -346,7 +344,6 @@ class CronJobManager:
tool_call_timeout=tool_call_timeout,
llm_safety_mode=False,
streaming_response=False,
tool_providers=[ComputerToolProvider()],
)
req = ProviderRequest()
conv = await _get_session_conv(event=cron_event, plugin_context=self.ctx)
+17 -6
View File
@@ -255,6 +255,13 @@ async def migration_persona_data(
logger.error(f"解析 Persona 配置失败:{e}")
def _get_dict_preference(key: str) -> dict[str, object]:
value = sp_v3.get(key, default={})
if not isinstance(value, dict):
raise TypeError(f"旧偏好设置 {key} 应为 dict, 实际为 {type(value).__name__}")
return value
async def migration_preferences(
db_helper: BaseDatabase,
platform_id_map: dict[str, dict[str, str]],
@@ -272,7 +279,7 @@ async def migration_preferences(
if value is not None:
await sp.put_async("global", "global", key, value)
logger.info(f"迁移全局偏好设置 {key} 成功,值: {value}")
session_conversation = sp_v3.get("session_conversation", default={})
session_conversation = _get_dict_preference("session_conversation")
for umo, conversation_id in session_conversation.items():
if not umo or not conversation_id:
continue
@@ -284,7 +291,7 @@ async def migration_preferences(
logger.info(f"迁移会话 {umo} 的对话数据到新表成功,平台 ID: {platform_id}")
except Exception as e:
logger.error(f"迁移会话 {umo} 的对话数据失败: {e}", exc_info=True)
session_service_config = sp_v3.get("session_service_config", default={})
session_service_config = _get_dict_preference("session_service_config")
for umo, config in session_service_config.items():
if not umo or not config:
continue
@@ -296,7 +303,7 @@ async def migration_preferences(
logger.info(f"迁移会话 {umo} 的服务配置到新表成功,平台 ID: {platform_id}")
except Exception as e:
logger.error(f"迁移会话 {umo} 的服务配置失败: {e}", exc_info=True)
session_variables = sp_v3.get("session_variables", default={})
session_variables = _get_dict_preference("session_variables")
for umo, variables in session_variables.items():
if not umo or not variables:
continue
@@ -307,7 +314,7 @@ async def migration_preferences(
await sp.put_async("umo", str(session), "session_variables", variables)
except Exception as e:
logger.error(f"迁移会话 {umo} 的变量失败: {e}", exc_info=True)
session_provider_perf = sp_v3.get("session_provider_perf", default={})
session_provider_perf = _get_dict_preference("session_provider_perf")
for umo, perf in session_provider_perf.items():
if not umo or not perf:
continue
@@ -315,8 +322,12 @@ async def migration_preferences(
session = MessageSesion.from_str(session_str=umo)
platform_id = get_platform_id(platform_id_map, session.platform_name)
session.platform_id = platform_id
perf_dict = perf
for provider_type, provider_id in perf_dict.items():
if not isinstance(perf, dict):
raise TypeError(
f"旧偏好设置 session_provider_perf.{umo} 应为 dict, "
f"实际为 {type(perf).__name__}",
)
for provider_type, provider_id in perf.items():
await sp.put_async(
"umo",
str(session),
+61 -34
View File
@@ -177,6 +177,20 @@ async def _install_requirements_with_precheck(
)
async def _get_global_list_preference(key: str) -> list[Any]:
value = await sp.global_get(key, [])
if not isinstance(value, list):
raise TypeError(f"全局偏好设置 {key} 应为 list, 实际为 {type(value).__name__}")
return value
async def _get_global_dict_preference(key: str) -> dict[Any, Any]:
value = await sp.global_get(key, {})
if not isinstance(value, dict):
raise TypeError(f"全局偏好设置 {key} 应为 dict, 实际为 {type(value).__name__}")
return value
class PluginManager:
def __init__(self, context: Context, config: AstrBotConfig) -> None:
self.updator = PluginUpdator()
@@ -470,6 +484,7 @@ class PluginManager:
Notes: 旧版本 AstrBot 插件可能使用的是 info() 函数来获取元数据。
"""
metadata = None
raw_metadata: object | None = None
if not os.path.exists(plugin_path):
raise Exception("插件不存在。")
@@ -479,52 +494,52 @@ class PluginManager:
os.path.join(plugin_path, "metadata.yaml"),
encoding="utf-8",
) as f:
metadata = yaml.safe_load(f)
raw_metadata = yaml.safe_load(f)
elif plugin_obj and hasattr(plugin_obj, "info"):
# 使用 info() 函数
metadata = plugin_obj.info()
raw_metadata = plugin_obj.info()
if isinstance(metadata, dict):
if "desc" not in metadata and "description" in metadata:
metadata["desc"] = metadata["description"]
if isinstance(raw_metadata, dict):
if "desc" not in raw_metadata and "description" in raw_metadata:
raw_metadata["desc"] = raw_metadata["description"]
if (
"name" not in metadata
or "desc" not in metadata
or "version" not in metadata
or "author" not in metadata
"name" not in raw_metadata
or "desc" not in raw_metadata
or "version" not in raw_metadata
or "author" not in raw_metadata
):
raise Exception(
"插件元数据信息不完整。name, desc, version, author 是必须的字段。",
)
metadata = StarMetadata(
name=metadata["name"],
author=metadata["author"],
desc=metadata["desc"],
name=raw_metadata["name"],
author=raw_metadata["author"],
desc=raw_metadata["desc"],
short_desc=(
metadata["short_desc"]
if isinstance(metadata.get("short_desc"), str)
raw_metadata["short_desc"]
if isinstance(raw_metadata.get("short_desc"), str)
else None
),
version=metadata["version"],
repo=metadata["repo"] if "repo" in metadata else None,
display_name=metadata.get("display_name", None),
version=raw_metadata["version"],
repo=raw_metadata["repo"] if "repo" in raw_metadata else None,
display_name=raw_metadata.get("display_name", None),
support_platforms=(
[
platform_id
for platform_id in metadata["support_platforms"]
for platform_id in raw_metadata["support_platforms"]
if isinstance(platform_id, str)
]
if isinstance(metadata.get("support_platforms"), list)
if isinstance(raw_metadata.get("support_platforms"), list)
else []
),
astrbot_version=(
metadata["astrbot_version"]
if isinstance(metadata.get("astrbot_version"), str)
raw_metadata["astrbot_version"]
if isinstance(raw_metadata.get("astrbot_version"), str)
else None
),
pages=metadata["pages"]
if isinstance(metadata.get("pages"), list)
pages=raw_metadata["pages"]
if isinstance(raw_metadata.get("pages"), list)
else [],
i18n=PluginManager._load_plugin_i18n(plugin_path),
)
@@ -888,9 +903,11 @@ class PluginManager:
- error_message (str|None): 错误信息,成功时为 None
"""
inactivated_plugins = await sp.global_get("inactivated_plugins", [])
inactivated_llm_tools = await sp.global_get("inactivated_llm_tools", [])
alter_cmd = await sp.global_get("alter_cmd", {})
inactivated_plugins = await _get_global_list_preference("inactivated_plugins")
inactivated_llm_tools = await _get_global_list_preference(
"inactivated_llm_tools",
)
alter_cmd = await _get_global_dict_preference("alter_cmd")
plugin_modules = self._get_plugin_modules()
if plugin_modules is None:
@@ -1145,6 +1162,8 @@ class PluginManager:
star_map[path] = metadata
star_registry.append(metadata)
assert metadata.module_path, f"插件 {metadata.name} 模块路径为空"
# 禁用/启用插件
if metadata.module_path in inactivated_plugins:
metadata.activated = False
@@ -1153,8 +1172,6 @@ class PluginManager:
if await asyncio.to_thread(os.path.exists, logo_path):
metadata.logo_path = logo_path
assert metadata.module_path, f"插件 {metadata.name} 模块路径为空"
full_names = []
for handler in star_handlers_registry.get_handlers_by_module_name(
metadata.module_path,
@@ -1163,7 +1180,8 @@ class PluginManager:
# 检查并且植入自定义的权限过滤器(alter_cmd)
if (
metadata.name in alter_cmd
metadata.name is not None
and metadata.name in alter_cmd
and handler.handler_name in alter_cmd[metadata.name]
):
cmd_type = alter_cmd[metadata.name][handler.handler_name].get(
@@ -1696,12 +1714,14 @@ class PluginManager:
await self._terminate_plugin(plugin)
# 加入到 shared_preferences 中
inactivated_plugins: list = await sp.global_get("inactivated_plugins", [])
inactivated_plugins = await _get_global_list_preference(
"inactivated_plugins",
)
if plugin.module_path not in inactivated_plugins:
inactivated_plugins.append(plugin.module_path)
inactivated_llm_tools: list = list(
set(await sp.global_get("inactivated_llm_tools", [])),
inactivated_llm_tools = list(
set(await _get_global_list_preference("inactivated_llm_tools")),
) # 后向兼容
# 禁用插件启用的 llm_tool
@@ -1769,12 +1789,19 @@ class PluginManager:
except Exception:
logger.error(traceback.format_exc())
async def cleanup_loaded_plugins(self) -> None:
"""Terminate all currently loaded plugin instances."""
for plugin in self.context.get_all_stars():
await self._terminate_plugin(plugin)
async def turn_on_plugin(self, plugin_name: str) -> None:
plugin = self.context.get_registered_star(plugin_name)
if plugin is None:
raise Exception(f"插件 {plugin_name} 不存在。")
inactivated_plugins: list = await sp.global_get("inactivated_plugins", [])
inactivated_llm_tools: list = await sp.global_get("inactivated_llm_tools", [])
inactivated_plugins = await _get_global_list_preference("inactivated_plugins")
inactivated_llm_tools = await _get_global_list_preference(
"inactivated_llm_tools",
)
if plugin.module_path in inactivated_plugins:
inactivated_plugins.remove(plugin.module_path)
await sp.global_put("inactivated_plugins", inactivated_plugins)
+16
View File
@@ -152,6 +152,22 @@ exclude = [
"**/__pycache__",
]
[tool.ty.src]
include = ["astrbot"]
exclude = [
"dashboard",
"node_modules",
"dist",
"data",
"tests",
"tests/**",
"**/tests/**",
"**/__pycache__",
]
[tool.ty.rules]
all = "ignore"
[tool.mypy]
plugins = ["pydantic.mypy"]
mypy_path = "types"