Skip to content

Commit c84bbc2

Browse files
no-teasyno-teasy
authored andcommitted
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
1 parent 3dd7799 commit c84bbc2

File tree

6 files changed

+85
-6
lines changed

6 files changed

+85
-6
lines changed

astrbot/core/computer/booters/local.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import subprocess
88
import sys
99
from dataclasses import dataclass
10-
from typing import Any
10+
from typing import Any, Callable
1111

1212
from astrbot.api import logger
1313
from astrbot.core.utils.astrbot_path import (
@@ -53,6 +53,45 @@ def _ensure_safe_path(path: str) -> str:
5353
return abs_path
5454

5555

56+
def _resolve_working_dir(configured_path: str | None, fallback_func: Callable[[], str]) -> tuple[str, bool]:
57+
"""Resolve working directory with fallback to default.
58+
59+
Args:
60+
configured_path: The configured working directory path, or None
61+
fallback_func: A callable that returns the fallback path (e.g., get_astrbot_root)
62+
63+
Returns:
64+
A tuple of (resolved_path, was_fallback) where was_fallback indicates if fallback was used
65+
"""
66+
if not configured_path:
67+
return fallback_func(), True
68+
69+
try:
70+
abs_path = _ensure_safe_path(configured_path)
71+
except PermissionError:
72+
logger.warning(
73+
f"[Computer] Configured path '{configured_path}' is outside allowed roots, "
74+
f"falling back to default directory."
75+
)
76+
return fallback_func(), True
77+
78+
if not os.path.exists(abs_path):
79+
logger.warning(
80+
f"[Computer] Configured path '{configured_path}' does not exist, "
81+
f"falling back to default directory."
82+
)
83+
return fallback_func(), True
84+
85+
if not os.access(abs_path, os.R_OK | os.W_OK):
86+
logger.warning(
87+
f"[Computer] Configured path '{configured_path}' is not accessible (no read/write permission), "
88+
f"falling back to default directory."
89+
)
90+
return fallback_func(), True
91+
92+
return abs_path, False
93+
94+
5695
def _decode_bytes_with_fallback(
5796
output: bytes | None,
5897
*,
@@ -110,7 +149,7 @@ def _run() -> dict[str, Any]:
110149
run_env = os.environ.copy()
111150
if env:
112151
run_env.update({str(k): str(v) for k, v in env.items()})
113-
working_dir = _ensure_safe_path(cwd) if cwd else get_astrbot_root()
152+
working_dir, _ = _resolve_working_dir(cwd, get_astrbot_root)
114153
if background:
115154
# `command` is intentionally executed through the current shell so
116155
# local computer-use behavior matches existing tool semantics.
@@ -146,20 +185,26 @@ def _run() -> dict[str, Any]:
146185

147186
@dataclass
148187
class LocalPythonComponent(PythonComponent):
188+
default_cwd: str | None = None
189+
149190
async def exec(
150191
self,
151192
code: str,
152193
kernel_id: str | None = None,
153194
timeout: int = 30,
154195
silent: bool = False,
196+
cwd: str | None = None,
155197
) -> dict[str, Any]:
156198
def _run() -> dict[str, Any]:
157199
try:
200+
effective_cwd = cwd if cwd else self.default_cwd
201+
working_dir, _ = _resolve_working_dir(effective_cwd, get_astrbot_root)
158202
result = subprocess.run(
159203
[os.environ.get("PYTHON", sys.executable), "-c", code],
160204
timeout=timeout,
161205
capture_output=True,
162206
text=True,
207+
cwd=working_dir,
163208
)
164209
stdout = "" if silent else result.stdout
165210
stderr = result.stderr if result.returncode != 0 else ""

astrbot/core/computer/olayer/python.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ async def exec(
1414
kernel_id: str | None = None,
1515
timeout: int = 30,
1616
silent: bool = False,
17+
cwd: str | None = None,
1718
) -> dict[str, Any]:
1819
"""Execute Python code"""
1920
...

astrbot/core/computer/tools/python.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult
6161
return resp
6262

6363

64+
def _get_configured_python_cwd(context: ContextWrapper[AstrAgentContext]) -> str | None:
65+
cfg = context.context.context.get_config(
66+
umo=context.context.event.unified_msg_origin
67+
)
68+
return cfg.get("provider_settings", {}).get("computer_use_local_python_cwd", None)
69+
70+
6471
@dataclass
6572
class PythonTool(FunctionTool):
6673
name: str = "astrbot_execute_ipython"
@@ -77,7 +84,8 @@ async def call(
7784
context.context.event.unified_msg_origin,
7885
)
7986
try:
80-
result = await sb.python.exec(code, silent=silent)
87+
cwd = _get_configured_python_cwd(context)
88+
result = await sb.python.exec(code, silent=silent, cwd=cwd)
8189
return await handle_result(result, context.context.event)
8290
except Exception as e:
8391
return f"Error executing code: {str(e)}"
@@ -100,7 +108,8 @@ async def call(
100108
return permission_error
101109
sb = get_local_booter()
102110
try:
103-
result = await sb.python.exec(code, silent=silent)
111+
cwd = _get_configured_python_cwd(context)
112+
result = await sb.python.exec(code, silent=silent, cwd=cwd)
104113
return await handle_result(result, context.context.event)
105114
except Exception as e:
106115
return f"Error executing code: {str(e)}"

astrbot/core/computer/tools/shell.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ class ExecuteShellTool(FunctionTool):
4040

4141
is_local: bool = False
4242

43+
def _get_configured_cwd(self, context: ContextWrapper[AstrAgentContext]) -> str | None:
44+
cfg = context.context.context.get_config(
45+
umo=context.context.event.unified_msg_origin
46+
)
47+
return cfg.get("provider_settings", {}).get("computer_use_local_shell_cwd", None)
48+
4349
async def call(
4450
self,
4551
context: ContextWrapper[AstrAgentContext],
@@ -58,7 +64,8 @@ async def call(
5864
context.context.event.unified_msg_origin,
5965
)
6066
try:
61-
result = await sb.shell.exec(command, background=background, env=env)
67+
cwd = self._get_configured_cwd(context)
68+
result = await sb.shell.exec(command, cwd=cwd, background=background, env=env)
6269
return json.dumps(result)
6370
except Exception as e:
6471
return f"Error executing command: {str(e)}"

astrbot/core/config/default.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3147,6 +3147,22 @@ class ChatProviderTemplate(TypedDict):
31473147
"type": "bool",
31483148
"hint": "开启后,需要 AstrBot 管理员权限才能调用使用电脑能力。在平台配置->管理员中可添加管理员。使用 /sid 指令查看管理员 ID。",
31493149
},
3150+
"provider_settings.computer_use_local_shell_cwd": {
3151+
"description": "本地 Shell 默认工作目录 / Local Shell Default Working Directory",
3152+
"type": "string",
3153+
"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.",
3154+
"condition": {
3155+
"provider_settings.computer_use_runtime": "local",
3156+
},
3157+
},
3158+
"provider_settings.computer_use_local_python_cwd": {
3159+
"description": "本地 Python 默认工作目录 / Local Python Default Working Directory",
3160+
"type": "string",
3161+
"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.",
3162+
"condition": {
3163+
"provider_settings.computer_use_runtime": "local",
3164+
},
3165+
},
31503166
"provider_settings.sandbox.booter": {
31513167
"description": "沙箱环境驱动器",
31523168
"type": "string",

astrbot/core/provider/sources/bailian_rerank_source.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ def _parse_results(self, data: dict) -> list[RerankResult]:
142142
f"百炼 API 错误: {data.get('code')}{data.get('message', '')}"
143143
)
144144

145-
results = data.get("output", {}).get("results", [])
145+
# 兼容旧版 API (output.results) 和新版 compatible API (results)
146+
results = (data.get("output") or {}).get("results") or data.get("results") or []
146147
if not results:
147148
logger.warning(f"百炼 Rerank 返回空结果: {data}")
148149
return []

0 commit comments

Comments
 (0)