Skip to content

Commit 524f507

Browse files
committed
Merge branch 'fix/provider-i18n' of https://github.com/Li-shi-ling/AstrBot into fix/provider-i18n
2 parents ad927b2 + 1d48ab4 commit 524f507

35 files changed

Lines changed: 724 additions & 246 deletions

File tree

astrbot/cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.18.1"
1+
__version__ = "4.18.2"

astrbot/core/astr_agent_tool_exec.py

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
from astrbot.core.astr_agent_context import AstrAgentContext
1818
from astrbot.core.astr_main_agent_resources import (
1919
BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT,
20+
EXECUTE_SHELL_TOOL,
21+
FILE_DOWNLOAD_TOOL,
22+
FILE_UPLOAD_TOOL,
23+
LOCAL_EXECUTE_SHELL_TOOL,
24+
LOCAL_PYTHON_TOOL,
25+
PYTHON_TOOL,
2026
SEND_MESSAGE_TO_USER_TOOL,
2127
)
2228
from astrbot.core.cron.events import CronMessageEvent
@@ -91,6 +97,65 @@ async def _run_in_background() -> None:
9197
yield r
9298
return
9399

100+
@classmethod
101+
def _get_runtime_computer_tools(cls, runtime: str) -> dict[str, FunctionTool]:
102+
if runtime == "sandbox":
103+
return {
104+
EXECUTE_SHELL_TOOL.name: EXECUTE_SHELL_TOOL,
105+
PYTHON_TOOL.name: PYTHON_TOOL,
106+
FILE_UPLOAD_TOOL.name: FILE_UPLOAD_TOOL,
107+
FILE_DOWNLOAD_TOOL.name: FILE_DOWNLOAD_TOOL,
108+
}
109+
if runtime == "local":
110+
return {
111+
LOCAL_EXECUTE_SHELL_TOOL.name: LOCAL_EXECUTE_SHELL_TOOL,
112+
LOCAL_PYTHON_TOOL.name: LOCAL_PYTHON_TOOL,
113+
}
114+
return {}
115+
116+
@classmethod
117+
def _build_handoff_toolset(
118+
cls,
119+
run_context: ContextWrapper[AstrAgentContext],
120+
tools: list[str | FunctionTool] | None,
121+
) -> ToolSet | None:
122+
ctx = run_context.context.context
123+
event = run_context.context.event
124+
cfg = ctx.get_config(umo=event.unified_msg_origin)
125+
provider_settings = cfg.get("provider_settings", {})
126+
runtime = str(provider_settings.get("computer_use_runtime", "local"))
127+
runtime_computer_tools = cls._get_runtime_computer_tools(runtime)
128+
129+
# Keep persona semantics aligned with the main agent: tools=None means
130+
# "all tools", including runtime computer-use tools.
131+
if tools is None:
132+
toolset = ToolSet()
133+
for registered_tool in llm_tools.func_list:
134+
if isinstance(registered_tool, HandoffTool):
135+
continue
136+
if registered_tool.active:
137+
toolset.add_tool(registered_tool)
138+
for runtime_tool in runtime_computer_tools.values():
139+
toolset.add_tool(runtime_tool)
140+
return None if toolset.empty() else toolset
141+
142+
if not tools:
143+
return None
144+
145+
toolset = ToolSet()
146+
for tool_name_or_obj in tools:
147+
if isinstance(tool_name_or_obj, str):
148+
registered_tool = llm_tools.get_func(tool_name_or_obj)
149+
if registered_tool and registered_tool.active:
150+
toolset.add_tool(registered_tool)
151+
continue
152+
runtime_tool = runtime_computer_tools.get(tool_name_or_obj)
153+
if runtime_tool:
154+
toolset.add_tool(runtime_tool)
155+
elif isinstance(tool_name_or_obj, FunctionTool):
156+
toolset.add_tool(tool_name_or_obj)
157+
return None if toolset.empty() else toolset
158+
94159
@classmethod
95160
async def _execute_handoff(
96161
cls,
@@ -101,19 +166,8 @@ async def _execute_handoff(
101166
input_ = tool_args.get("input")
102167
image_urls = tool_args.get("image_urls")
103168

104-
# make toolset for the agent
105-
tools = tool.agent.tools
106-
if tools:
107-
toolset = ToolSet()
108-
for t in tools:
109-
if isinstance(t, str):
110-
_t = llm_tools.get_func(t)
111-
if _t:
112-
toolset.add_tool(_t)
113-
elif isinstance(t, FunctionTool):
114-
toolset.add_tool(t)
115-
else:
116-
toolset = None
169+
# Build handoff toolset from registered tools plus runtime computer tools.
170+
toolset = cls._build_handoff_toolset(run_context, tool.agent.tools)
117171

118172
ctx = run_context.context.context
119173
event = run_context.context.event

astrbot/core/computer/tools/fs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
1212

1313
from ..computer_client import get_booter
14+
from .permissions import check_admin_permission
1415

1516
# @dataclass
1617
# class CreateFileTool(FunctionTool):
@@ -102,6 +103,8 @@ async def call(
102103
context: ContextWrapper[AstrAgentContext],
103104
local_path: str,
104105
) -> str | None:
106+
if permission_error := check_admin_permission(context, "File upload/download"):
107+
return permission_error
105108
sb = await get_booter(
106109
context.context.context,
107110
context.context.event.unified_msg_origin,
@@ -161,6 +164,8 @@ async def call(
161164
remote_path: str,
162165
also_send_to_user: bool = True,
163166
) -> ToolExecResult:
167+
if permission_error := check_admin_permission(context, "File upload/download"):
168+
return permission_error
164169
sb = await get_booter(
165170
context.context.context,
166171
context.context.event.unified_msg_origin,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from astrbot.core.agent.run_context import ContextWrapper
2+
from astrbot.core.astr_agent_context import AstrAgentContext
3+
4+
5+
def check_admin_permission(
6+
context: ContextWrapper[AstrAgentContext], operation_name: str
7+
) -> str | None:
8+
cfg = context.context.context.get_config(
9+
umo=context.context.event.unified_msg_origin
10+
)
11+
provider_settings = cfg.get("provider_settings", {})
12+
require_admin = provider_settings.get("computer_use_require_admin", True)
13+
if require_admin and context.context.event.role != "admin":
14+
return (
15+
f"error: Permission denied. {operation_name} is only allowed for admin users. "
16+
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` by adding their user ID to the admins list if they need this feature. "
17+
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
18+
)
19+
return None

astrbot/core/computer/tools/python.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from astrbot.core.agent.tool import ToolExecResult
88
from astrbot.core.astr_agent_context import AstrAgentContext, AstrMessageEvent
99
from astrbot.core.computer.computer_client import get_booter, get_local_booter
10+
from astrbot.core.computer.tools.permissions import check_admin_permission
1011
from astrbot.core.message.message_event_result import MessageChain
1112

1213
param_schema = {
@@ -26,21 +27,6 @@
2627
}
2728

2829

29-
def _check_admin_permission(context: ContextWrapper[AstrAgentContext]) -> str | None:
30-
cfg = context.context.context.get_config(
31-
umo=context.context.event.unified_msg_origin
32-
)
33-
provider_settings = cfg.get("provider_settings", {})
34-
require_admin = provider_settings.get("computer_use_require_admin", True)
35-
if require_admin and context.context.event.role != "admin":
36-
return (
37-
"error: Permission denied. Python execution is only allowed for admin users. "
38-
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` by adding their user ID to the admins list if they need this feature."
39-
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
40-
)
41-
return None
42-
43-
4430
async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult:
4531
data = result.get("data", {})
4632
output = data.get("output", {})
@@ -81,7 +67,7 @@ class PythonTool(FunctionTool):
8167
async def call(
8268
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
8369
) -> ToolExecResult:
84-
if permission_error := _check_admin_permission(context):
70+
if permission_error := check_admin_permission(context, "Python execution"):
8571
return permission_error
8672
sb = await get_booter(
8773
context.context.context,
@@ -104,7 +90,7 @@ class LocalPythonTool(FunctionTool):
10490
async def call(
10591
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
10692
) -> ToolExecResult:
107-
if permission_error := _check_admin_permission(context):
93+
if permission_error := check_admin_permission(context, "Python execution"):
10894
return permission_error
10995
sb = get_local_booter()
11096
try:

astrbot/core/computer/tools/shell.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,7 @@
77
from astrbot.core.astr_agent_context import AstrAgentContext
88

99
from ..computer_client import get_booter, get_local_booter
10-
11-
12-
def _check_admin_permission(context: ContextWrapper[AstrAgentContext]) -> str | None:
13-
cfg = context.context.context.get_config(
14-
umo=context.context.event.unified_msg_origin
15-
)
16-
provider_settings = cfg.get("provider_settings", {})
17-
require_admin = provider_settings.get("computer_use_require_admin", True)
18-
if require_admin and context.context.event.role != "admin":
19-
return (
20-
"error: Permission denied. Shell execution is only allowed for admin users. "
21-
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` by adding their user ID to the admins list if they need this feature."
22-
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
23-
)
24-
return None
10+
from .permissions import check_admin_permission
2511

2612

2713
@dataclass
@@ -61,7 +47,7 @@ async def call(
6147
background: bool = False,
6248
env: dict = {},
6349
) -> ToolExecResult:
64-
if permission_error := _check_admin_permission(context):
50+
if permission_error := check_admin_permission(context, "Shell execution"):
6551
return permission_error
6652

6753
if self.is_local:

astrbot/core/config/default.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
77

8-
VERSION = "4.18.1"
8+
VERSION = "4.18.2"
99
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
1010

1111
WEBHOOK_SUPPORTED_PLATFORMS = [

astrbot/core/message/components.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -720,13 +720,38 @@ async def get_file(self, allow_return_url: bool = False) -> str:
720720
if allow_return_url and self.url:
721721
return self.url
722722

723-
if self.file_ and os.path.exists(self.file_):
724-
return os.path.abspath(self.file_)
723+
if self.file_:
724+
path = self.file_
725+
if path.startswith("file://"):
726+
# 处理 file:// (2 slashes) 或 file:/// (3 slashes)
727+
# pathlib.as_uri() 通常生成 file:///
728+
path = path[7:]
729+
# 兼容 Windows: file:///C:/path -> /C:/path -> C:/path
730+
if (
731+
os.name == "nt"
732+
and len(path) > 2
733+
and path[0] == "/"
734+
and path[2] == ":"
735+
):
736+
path = path[1:]
737+
738+
if os.path.exists(path):
739+
return os.path.abspath(path)
725740

726741
if self.url:
727742
await self._download_file()
728743
if self.file_:
729-
return os.path.abspath(self.file_)
744+
path = self.file_
745+
if path.startswith("file://"):
746+
path = path[7:]
747+
if (
748+
os.name == "nt"
749+
and len(path) > 2
750+
and path[0] == "/"
751+
and path[2] == ":"
752+
):
753+
path = path[1:]
754+
return os.path.abspath(path)
730755

731756
return ""
732757

astrbot/core/pipeline/__init__.py

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,60 @@
1+
"""Pipeline package exports.
2+
3+
This module intentionally avoids eager imports of all pipeline stage modules to
4+
prevent import-time cycles. Stage classes remain available via lazy attribute
5+
resolution for backward compatibility.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from importlib import import_module
11+
from typing import Any
12+
113
from astrbot.core.message.message_event_result import (
214
EventResultType,
315
MessageEventResult,
416
)
517

6-
from .content_safety_check.stage import ContentSafetyCheckStage
7-
from .preprocess_stage.stage import PreProcessStage
8-
from .process_stage.stage import ProcessStage
9-
from .rate_limit_check.stage import RateLimitStage
10-
from .respond.stage import RespondStage
11-
from .result_decorate.stage import ResultDecorateStage
12-
from .session_status_check.stage import SessionStatusCheckStage
13-
from .waking_check.stage import WakingCheckStage
14-
from .whitelist_check.stage import WhitelistCheckStage
15-
16-
# 管道阶段顺序
17-
STAGES_ORDER = [
18-
"WakingCheckStage", # 检查是否需要唤醒
19-
"WhitelistCheckStage", # 检查是否在群聊/私聊白名单
20-
"SessionStatusCheckStage", # 检查会话是否整体启用
21-
"RateLimitStage", # 检查会话是否超过频率限制
22-
"ContentSafetyCheckStage", # 检查内容安全
23-
"PreProcessStage", # 预处理
24-
"ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用
25-
"ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等
26-
"RespondStage", # 发送消息
27-
]
18+
from .stage_order import STAGES_ORDER
19+
20+
_LAZY_EXPORTS = {
21+
"ContentSafetyCheckStage": (
22+
"astrbot.core.pipeline.content_safety_check.stage",
23+
"ContentSafetyCheckStage",
24+
),
25+
"PreProcessStage": (
26+
"astrbot.core.pipeline.preprocess_stage.stage",
27+
"PreProcessStage",
28+
),
29+
"ProcessStage": (
30+
"astrbot.core.pipeline.process_stage.stage",
31+
"ProcessStage",
32+
),
33+
"RateLimitStage": (
34+
"astrbot.core.pipeline.rate_limit_check.stage",
35+
"RateLimitStage",
36+
),
37+
"RespondStage": (
38+
"astrbot.core.pipeline.respond.stage",
39+
"RespondStage",
40+
),
41+
"ResultDecorateStage": (
42+
"astrbot.core.pipeline.result_decorate.stage",
43+
"ResultDecorateStage",
44+
),
45+
"SessionStatusCheckStage": (
46+
"astrbot.core.pipeline.session_status_check.stage",
47+
"SessionStatusCheckStage",
48+
),
49+
"WakingCheckStage": (
50+
"astrbot.core.pipeline.waking_check.stage",
51+
"WakingCheckStage",
52+
),
53+
"WhitelistCheckStage": (
54+
"astrbot.core.pipeline.whitelist_check.stage",
55+
"WhitelistCheckStage",
56+
),
57+
}
2858

2959
__all__ = [
3060
"ContentSafetyCheckStage",
@@ -36,6 +66,21 @@
3666
"RespondStage",
3767
"ResultDecorateStage",
3868
"SessionStatusCheckStage",
69+
"STAGES_ORDER",
3970
"WakingCheckStage",
4071
"WhitelistCheckStage",
4172
]
73+
74+
75+
def __getattr__(name: str) -> Any:
76+
if name not in _LAZY_EXPORTS:
77+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
78+
module_path, attr_name = _LAZY_EXPORTS[name]
79+
module = import_module(module_path)
80+
value = getattr(module, attr_name)
81+
globals()[name] = value
82+
return value
83+
84+
85+
def __dir__() -> list[str]:
86+
return sorted(set(globals()) | set(__all__))

0 commit comments

Comments
 (0)