From c84bbc2eb5bbd29a44b33591f426528d4701c00e Mon Sep 17 00:00:00 2001 From: JiangChe <3992412947@qq.com> Date: Wed, 1 Apr 2026 11:55:54 +0800 Subject: [PATCH 1/3] feat: add configurable default working directory for local shell and python execution - Add computer_use_local_shell_cwd and computer_use_local_python_cwd config options - Only effective when computer_use_runtime=local - Falls back to AstrBot root directory if path is invalid or inaccessible - Add WebUI config with Chinese and English descriptions - Refactor _resolve_working_dir to use _ensure_safe_path - Add proper type annotations for Callable - Add explicit None default for config get methods to prevent KeyError --- astrbot/core/computer/booters/local.py | 49 ++++++++++++++++++- astrbot/core/computer/olayer/python.py | 1 + astrbot/core/computer/tools/python.py | 13 ++++- astrbot/core/computer/tools/shell.py | 9 +++- astrbot/core/config/default.py | 16 ++++++ .../provider/sources/bailian_rerank_source.py | 3 +- 6 files changed, 85 insertions(+), 6 deletions(-) diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index f11bc329fa..4439ec42c4 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -7,7 +7,7 @@ import subprocess import sys from dataclasses import dataclass -from typing import Any +from typing import Any, Callable from astrbot.api import logger from astrbot.core.utils.astrbot_path import ( @@ -53,6 +53,45 @@ def _ensure_safe_path(path: str) -> str: return abs_path +def _resolve_working_dir(configured_path: str | None, fallback_func: Callable[[], str]) -> tuple[str, bool]: + """Resolve working directory with fallback to default. + + Args: + configured_path: The configured working directory path, or None + fallback_func: A callable that returns the fallback path (e.g., get_astrbot_root) + + Returns: + A tuple of (resolved_path, was_fallback) where was_fallback indicates if fallback was used + """ + if not configured_path: + return fallback_func(), True + + try: + abs_path = _ensure_safe_path(configured_path) + except PermissionError: + logger.warning( + f"[Computer] Configured path '{configured_path}' is outside allowed roots, " + f"falling back to default directory." + ) + return fallback_func(), True + + if not os.path.exists(abs_path): + logger.warning( + f"[Computer] Configured path '{configured_path}' does not exist, " + f"falling back to default directory." + ) + return fallback_func(), True + + if not os.access(abs_path, os.R_OK | os.W_OK): + logger.warning( + f"[Computer] Configured path '{configured_path}' is not accessible (no read/write permission), " + f"falling back to default directory." + ) + return fallback_func(), True + + return abs_path, False + + def _decode_bytes_with_fallback( output: bytes | None, *, @@ -110,7 +149,7 @@ def _run() -> dict[str, Any]: run_env = os.environ.copy() if env: run_env.update({str(k): str(v) for k, v in env.items()}) - working_dir = _ensure_safe_path(cwd) if cwd else get_astrbot_root() + working_dir, _ = _resolve_working_dir(cwd, get_astrbot_root) if background: # `command` is intentionally executed through the current shell so # local computer-use behavior matches existing tool semantics. @@ -146,20 +185,26 @@ def _run() -> dict[str, Any]: @dataclass class LocalPythonComponent(PythonComponent): + default_cwd: str | None = None + async def exec( self, code: str, kernel_id: str | None = None, timeout: int = 30, silent: bool = False, + cwd: str | None = None, ) -> dict[str, Any]: def _run() -> dict[str, Any]: try: + effective_cwd = cwd if cwd else self.default_cwd + working_dir, _ = _resolve_working_dir(effective_cwd, get_astrbot_root) result = subprocess.run( [os.environ.get("PYTHON", sys.executable), "-c", code], timeout=timeout, capture_output=True, text=True, + cwd=working_dir, ) stdout = "" if silent else result.stdout stderr = result.stderr if result.returncode != 0 else "" diff --git a/astrbot/core/computer/olayer/python.py b/astrbot/core/computer/olayer/python.py index 6255041463..2b86b11530 100644 --- a/astrbot/core/computer/olayer/python.py +++ b/astrbot/core/computer/olayer/python.py @@ -14,6 +14,7 @@ async def exec( kernel_id: str | None = None, timeout: int = 30, silent: bool = False, + cwd: str | None = None, ) -> dict[str, Any]: """Execute Python code""" ... diff --git a/astrbot/core/computer/tools/python.py b/astrbot/core/computer/tools/python.py index bf9aaa14e5..d1b9626c66 100644 --- a/astrbot/core/computer/tools/python.py +++ b/astrbot/core/computer/tools/python.py @@ -61,6 +61,13 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult return resp +def _get_configured_python_cwd(context: ContextWrapper[AstrAgentContext]) -> str | None: + cfg = context.context.context.get_config( + umo=context.context.event.unified_msg_origin + ) + return cfg.get("provider_settings", {}).get("computer_use_local_python_cwd", None) + + @dataclass class PythonTool(FunctionTool): name: str = "astrbot_execute_ipython" @@ -77,7 +84,8 @@ async def call( context.context.event.unified_msg_origin, ) try: - result = await sb.python.exec(code, silent=silent) + cwd = _get_configured_python_cwd(context) + result = await sb.python.exec(code, silent=silent, cwd=cwd) return await handle_result(result, context.context.event) except Exception as e: return f"Error executing code: {str(e)}" @@ -100,7 +108,8 @@ async def call( return permission_error sb = get_local_booter() try: - result = await sb.python.exec(code, silent=silent) + cwd = _get_configured_python_cwd(context) + result = await sb.python.exec(code, silent=silent, cwd=cwd) return await handle_result(result, context.context.event) except Exception as e: return f"Error executing code: {str(e)}" diff --git a/astrbot/core/computer/tools/shell.py b/astrbot/core/computer/tools/shell.py index b5009d30fd..1d1cbe7912 100644 --- a/astrbot/core/computer/tools/shell.py +++ b/astrbot/core/computer/tools/shell.py @@ -40,6 +40,12 @@ class ExecuteShellTool(FunctionTool): is_local: bool = False + def _get_configured_cwd(self, context: ContextWrapper[AstrAgentContext]) -> str | None: + cfg = context.context.context.get_config( + umo=context.context.event.unified_msg_origin + ) + return cfg.get("provider_settings", {}).get("computer_use_local_shell_cwd", None) + async def call( self, context: ContextWrapper[AstrAgentContext], @@ -58,7 +64,8 @@ async def call( context.context.event.unified_msg_origin, ) try: - result = await sb.shell.exec(command, background=background, env=env) + cwd = self._get_configured_cwd(context) + result = await sb.shell.exec(command, cwd=cwd, background=background, env=env) return json.dumps(result) except Exception as e: return f"Error executing command: {str(e)}" diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 45412bdccb..717fe08745 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -3147,6 +3147,22 @@ class ChatProviderTemplate(TypedDict): "type": "bool", "hint": "开启后,需要 AstrBot 管理员权限才能调用使用电脑能力。在平台配置->管理员中可添加管理员。使用 /sid 指令查看管理员 ID。", }, + "provider_settings.computer_use_local_shell_cwd": { + "description": "本地 Shell 默认工作目录 / Local Shell Default Working Directory", + "type": "string", + "hint": "zh: 设置本地 shell 命令执行时的默认工作目录。仅在 computer_use_runtime=local 时生效。留空则使用 AstrBot 根目录。\nen: Set the default working directory for local shell command execution. Only effective when computer_use_runtime=local. If empty, uses AstrBot root directory.", + "condition": { + "provider_settings.computer_use_runtime": "local", + }, + }, + "provider_settings.computer_use_local_python_cwd": { + "description": "本地 Python 默认工作目录 / Local Python Default Working Directory", + "type": "string", + "hint": "zh: 设置本地 Python 代码执行时的默认工作目录。仅在 computer_use_runtime=local 时生效。留空则使用 AstrBot 根目录。\nen: Set the default working directory for local Python code execution. Only effective when computer_use_runtime=local. If empty, uses AstrBot root directory.", + "condition": { + "provider_settings.computer_use_runtime": "local", + }, + }, "provider_settings.sandbox.booter": { "description": "沙箱环境驱动器", "type": "string", diff --git a/astrbot/core/provider/sources/bailian_rerank_source.py b/astrbot/core/provider/sources/bailian_rerank_source.py index a515d2b9d2..4513c2c850 100644 --- a/astrbot/core/provider/sources/bailian_rerank_source.py +++ b/astrbot/core/provider/sources/bailian_rerank_source.py @@ -142,7 +142,8 @@ def _parse_results(self, data: dict) -> list[RerankResult]: f"百炼 API 错误: {data.get('code')} – {data.get('message', '')}" ) - results = data.get("output", {}).get("results", []) + # 兼容旧版 API (output.results) 和新版 compatible API (results) + results = (data.get("output") or {}).get("results") or data.get("results") or [] if not results: logger.warning(f"百炼 Rerank 返回空结果: {data}") return [] From feaccd7c55f5e797131e434405f5e883fa301894 Mon Sep 17 00:00:00 2001 From: no-teasy <399212947@qq.com> Date: Wed, 1 Apr 2026 23:38:35 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor(computer):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=85=AC=E5=85=B1=E9=85=8D=E7=BD=AE=E7=9B=AE=E5=BD=95=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E9=80=BB=E8=BE=91=E5=88=B0=E5=B7=A5=E5=85=B7=E5=87=BD?= =?UTF-8?q?=E6=95=B0https://github.com/AstrBotDevs/AstrBot/pull/7281#pullr?= =?UTF-8?q?equestreview-4044873878?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将重复的配置目录获取逻辑提取到 permissions.py 中的 get_configured_cwd 函数 移除 LocalPythonComponent 中不再需要的 default_cwd 字段 --- astrbot/core/computer/booters/local.py | 5 +---- astrbot/core/computer/tools/permissions.py | 9 +++++++++ astrbot/core/computer/tools/python.py | 7 ++----- astrbot/core/computer/tools/shell.py | 7 ++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index 4439ec42c4..97d02e8edb 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -185,8 +185,6 @@ def _run() -> dict[str, Any]: @dataclass class LocalPythonComponent(PythonComponent): - default_cwd: str | None = None - async def exec( self, code: str, @@ -197,8 +195,7 @@ async def exec( ) -> dict[str, Any]: def _run() -> dict[str, Any]: try: - effective_cwd = cwd if cwd else self.default_cwd - working_dir, _ = _resolve_working_dir(effective_cwd, get_astrbot_root) + working_dir, _ = _resolve_working_dir(cwd, get_astrbot_root) result = subprocess.run( [os.environ.get("PYTHON", sys.executable), "-c", code], timeout=timeout, diff --git a/astrbot/core/computer/tools/permissions.py b/astrbot/core/computer/tools/permissions.py index 489f485f9d..592836a0be 100644 --- a/astrbot/core/computer/tools/permissions.py +++ b/astrbot/core/computer/tools/permissions.py @@ -2,6 +2,15 @@ from astrbot.core.astr_agent_context import AstrAgentContext +def get_configured_cwd( + context: ContextWrapper[AstrAgentContext], config_key: str +) -> str | None: + cfg = context.context.context.get_config( + umo=context.context.event.unified_msg_origin + ) + return cfg.get("provider_settings", {}).get(config_key, None) + + def check_admin_permission( context: ContextWrapper[AstrAgentContext], operation_name: str ) -> str | None: diff --git a/astrbot/core/computer/tools/python.py b/astrbot/core/computer/tools/python.py index d1b9626c66..01dcfa7202 100644 --- a/astrbot/core/computer/tools/python.py +++ b/astrbot/core/computer/tools/python.py @@ -8,7 +8,7 @@ from astrbot.core.agent.tool import ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext, AstrMessageEvent from astrbot.core.computer.computer_client import get_booter, get_local_booter -from astrbot.core.computer.tools.permissions import check_admin_permission +from astrbot.core.computer.tools.permissions import check_admin_permission, get_configured_cwd from astrbot.core.message.message_event_result import MessageChain _OS_NAME = platform.system() @@ -62,10 +62,7 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult def _get_configured_python_cwd(context: ContextWrapper[AstrAgentContext]) -> str | None: - cfg = context.context.context.get_config( - umo=context.context.event.unified_msg_origin - ) - return cfg.get("provider_settings", {}).get("computer_use_local_python_cwd", None) + return get_configured_cwd(context, "computer_use_local_python_cwd") @dataclass diff --git a/astrbot/core/computer/tools/shell.py b/astrbot/core/computer/tools/shell.py index 1d1cbe7912..87c57c7f5d 100644 --- a/astrbot/core/computer/tools/shell.py +++ b/astrbot/core/computer/tools/shell.py @@ -7,7 +7,7 @@ from astrbot.core.astr_agent_context import AstrAgentContext from ..computer_client import get_booter, get_local_booter -from .permissions import check_admin_permission +from .permissions import check_admin_permission, get_configured_cwd @dataclass @@ -41,10 +41,7 @@ class ExecuteShellTool(FunctionTool): is_local: bool = False def _get_configured_cwd(self, context: ContextWrapper[AstrAgentContext]) -> str | None: - cfg = context.context.context.get_config( - umo=context.context.event.unified_msg_origin - ) - return cfg.get("provider_settings", {}).get("computer_use_local_shell_cwd", None) + return get_configured_cwd(context, "computer_use_local_shell_cwd") async def call( self, From 4027630e6d5f6abf243dfd8f4c176755b8fcc410 Mon Sep 17 00:00:00 2001 From: JiangChe <3992412947@qq.com> Date: Fri, 10 Apr 2026 11:15:09 +0000 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8DPR=20CI=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- astrbot/core/computer/booters/local.py | 7 +++++-- astrbot/core/computer/tools/python.py | 5 ++++- astrbot/core/computer/tools/shell.py | 8 ++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index 97d02e8edb..17dec74727 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -6,8 +6,9 @@ import shutil import subprocess import sys +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable +from typing import Any from astrbot.api import logger from astrbot.core.utils.astrbot_path import ( @@ -53,7 +54,9 @@ def _ensure_safe_path(path: str) -> str: return abs_path -def _resolve_working_dir(configured_path: str | None, fallback_func: Callable[[], str]) -> tuple[str, bool]: +def _resolve_working_dir( + configured_path: str | None, fallback_func: Callable[[], str] +) -> tuple[str, bool]: """Resolve working directory with fallback to default. Args: diff --git a/astrbot/core/computer/tools/python.py b/astrbot/core/computer/tools/python.py index 01dcfa7202..2aff98713a 100644 --- a/astrbot/core/computer/tools/python.py +++ b/astrbot/core/computer/tools/python.py @@ -8,7 +8,10 @@ from astrbot.core.agent.tool import ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext, AstrMessageEvent from astrbot.core.computer.computer_client import get_booter, get_local_booter -from astrbot.core.computer.tools.permissions import check_admin_permission, get_configured_cwd +from astrbot.core.computer.tools.permissions import ( + check_admin_permission, + get_configured_cwd, +) from astrbot.core.message.message_event_result import MessageChain _OS_NAME = platform.system() diff --git a/astrbot/core/computer/tools/shell.py b/astrbot/core/computer/tools/shell.py index 87c57c7f5d..b51bdbb888 100644 --- a/astrbot/core/computer/tools/shell.py +++ b/astrbot/core/computer/tools/shell.py @@ -40,7 +40,9 @@ class ExecuteShellTool(FunctionTool): is_local: bool = False - def _get_configured_cwd(self, context: ContextWrapper[AstrAgentContext]) -> str | None: + def _get_configured_cwd( + self, context: ContextWrapper[AstrAgentContext] + ) -> str | None: return get_configured_cwd(context, "computer_use_local_shell_cwd") async def call( @@ -62,7 +64,9 @@ async def call( ) try: cwd = self._get_configured_cwd(context) - result = await sb.shell.exec(command, cwd=cwd, background=background, env=env) + result = await sb.shell.exec( + command, cwd=cwd, background=background, env=env + ) return json.dumps(result) except Exception as e: return f"Error executing command: {str(e)}"