-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat:使用电脑能力支持配置默认工作目录 #7281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat:使用电脑能力支持配置默认工作目录 #7281
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
+78
to
+92
|
||
|
|
||
|
|
||
| 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. | ||
|
|
@@ -152,14 +191,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 "" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() | ||
|
|
@@ -61,6 +61,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 +81,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) | ||
|
Comment on lines
83
to
86
|
||
| except Exception as e: | ||
| return f"Error executing code: {str(e)}" | ||
|
|
@@ -100,7 +105,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)}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
| }, | ||
| }, | ||
|
Comment on lines
+3150
to
+3165
|
||
| "provider_settings.sandbox.booter": { | ||
| "description": "沙箱环境驱动器", | ||
| "type": "string", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||
|
||||||
| # 兼容旧版 API (output.results) 和新版 compatible API (results) | |
| # 兼容旧版 API(output.results)和新版 API(results) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_resolve_working_dir()relies on_ensure_safe_path()for root checks, but_ensure_safe_path()usesos.path.abspath()+startswith(), which can be bypassed via symlinks (and also via prefix paths like/allowed_root_tmp). Since this PR exposes configurable working dirs, consider switching torealpath/Path.resolve()and anos.path.commonpath()-based containment check to enforce allowed roots correctly.