diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index f11bc329fa..17dec74727 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -6,6 +6,7 @@ import shutil import subprocess import sys +from collections.abc import Callable from dataclasses import dataclass from typing import Any @@ -53,6 +54,47 @@ 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 +152,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. @@ -152,14 +194,17 @@ async def exec( kernel_id: str | None = None, timeout: int = 30, silent: bool = False, + cwd: str | None = None, ) -> dict[str, Any]: def _run() -> dict[str, Any]: try: + working_dir, _ = _resolve_working_dir(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/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 bf9aaa14e5..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 +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() @@ -61,6 +64,10 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult return resp +def _get_configured_python_cwd(context: ContextWrapper[AstrAgentContext]) -> str | None: + return get_configured_cwd(context, "computer_use_local_python_cwd") + + @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..b51bdbb888 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 @@ -40,6 +40,11 @@ class ExecuteShellTool(FunctionTool): is_local: bool = False + def _get_configured_cwd( + self, context: ContextWrapper[AstrAgentContext] + ) -> str | None: + return get_configured_cwd(context, "computer_use_local_shell_cwd") + async def call( self, context: ContextWrapper[AstrAgentContext], @@ -58,7 +63,10 @@ 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 []