From f16edd4ffff060feb74b1deaadb616de7fea1c2e Mon Sep 17 00:00:00 2001 From: zenfun Date: Thu, 12 Mar 2026 02:34:47 +0800 Subject: [PATCH] refactor: delegate tool injection to booter self-description API - Add get_default_tools/get_tools/get_system_prompt_parts to ComputerBooter base - Each booter subclass (ShipyardNeo, Shipyard, Boxlite) declares its own tools - ComputerToolProvider now delegates to booter API via computer_client helpers - Add unified query API: get_sandbox_tools, get_default_sandbox_tools, etc. - Extract Neo prompts to dedicated computer/prompts.py module - Add booter type constants (booters/constants.py) - Fix subagent tool path to pass sandbox_cfg and session_id - Fix Sourcery issues: shell injection in send_message, typo in prompts, internal tools bypass inactivated_llm_tools check --- astrbot/core/astr_agent_tool_exec.py | 20 ++- astrbot/core/astr_main_agent.py | 7 +- astrbot/core/computer/booters/base.py | 22 +++ astrbot/core/computer/booters/boxlite.py | 29 +++- astrbot/core/computer/booters/constants.py | 3 + astrbot/core/computer/booters/shipyard.py | 29 ++++ astrbot/core/computer/booters/shipyard_neo.py | 83 ++++++++- astrbot/core/computer/computer_client.py | 60 +++++++ .../core/computer/computer_tool_provider.py | 164 ++++-------------- astrbot/core/computer/prompts.py | 24 +++ astrbot/core/tools/prompts.py | 2 +- astrbot/core/tools/send_message.py | 4 +- 12 files changed, 312 insertions(+), 135 deletions(-) create mode 100644 astrbot/core/computer/booters/constants.py create mode 100644 astrbot/core/computer/prompts.py diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 57f2b34b8..27d6aeae3 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -174,12 +174,21 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): return @classmethod - def _get_runtime_computer_tools(cls, runtime: str) -> dict[str, FunctionTool]: + def _get_runtime_computer_tools( + cls, + runtime: str, + sandbox_cfg: dict | None = None, + session_id: str = "", + ) -> dict[str, FunctionTool]: from astrbot.core.computer.computer_tool_provider import ComputerToolProvider from astrbot.core.tool_provider import ToolProviderContext provider = ComputerToolProvider() - ctx = ToolProviderContext(computer_use_runtime=runtime) + ctx = ToolProviderContext( + computer_use_runtime=runtime, + sandbox_cfg=sandbox_cfg, + session_id=session_id, + ) tools = provider.get_tools(ctx) return {tool.name: tool for tool in tools} @@ -194,7 +203,12 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): cfg = ctx.get_config(umo=event.unified_msg_origin) provider_settings = cfg.get("provider_settings", {}) runtime = str(provider_settings.get("computer_use_runtime", "local")) - runtime_computer_tools = cls._get_runtime_computer_tools(runtime) + sandbox_cfg = provider_settings.get("sandbox", {}) + runtime_computer_tools = cls._get_runtime_computer_tools( + runtime, + sandbox_cfg=sandbox_cfg, + session_id=event.unified_msg_origin, + ) # Keep persona semantics aligned with the main agent: tools=None means # "all tools", including runtime computer-use tools. diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 3613aeb03..f65fb9919 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -1033,7 +1033,9 @@ async def build_main_agent( sandbox_cfg=config.sandbox_cfg, session_id=req.session_id or "", ) - # Respect WebUI tool enable/disable settings + # Respect WebUI tool enable/disable settings. + # Internal tools (source='internal') bypass this check — they are + # not user-togglable in the WebUI, so legacy entries must not block them. _inactivated: set[str] = set( sp.get("inactivated_llm_tools", [], scope="global", scope_id="global") ) @@ -1043,7 +1045,8 @@ async def build_main_agent( if req.func_tool is None: req.func_tool = ToolSet() for _tool in _tp_tools: - if _tool.name not in _inactivated: + is_internal = getattr(_tool, "source", "") == "internal" + if is_internal or _tool.name not in _inactivated: req.func_tool.add_tool(_tool) _tp_addon = _tp.get_system_prompt_addon(_provider_ctx) if _tp_addon: diff --git a/astrbot/core/computer/booters/base.py b/astrbot/core/computer/booters/base.py index 4c74e5edd..29eb2281f 100644 --- a/astrbot/core/computer/booters/base.py +++ b/astrbot/core/computer/booters/base.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from ..olayer import ( BrowserComponent, FileSystemComponent, @@ -5,6 +9,9 @@ from ..olayer import ( ShellComponent, ) +if TYPE_CHECKING: + from astrbot.core.agent.tool import FunctionTool + class ComputerBooter: @property @@ -47,3 +54,18 @@ class ComputerBooter: async def available(self) -> bool: """Check if the computer is available.""" ... + + @classmethod + def get_default_tools(cls) -> list[FunctionTool]: + """Conservative full tool list (no instance needed, pre-boot).""" + return [] + + def get_tools(self) -> list[FunctionTool]: + """Capability-filtered tool list (post-boot). + Defaults to get_default_tools().""" + return self.__class__.get_default_tools() + + @classmethod + def get_system_prompt_parts(cls) -> list[str]: + """Booter-specific system prompt fragments (static text, no instance needed).""" + return [] diff --git a/astrbot/core/computer/booters/boxlite.py b/astrbot/core/computer/booters/boxlite.py index 70064fdd4..55518bfd4 100644 --- a/astrbot/core/computer/booters/boxlite.py +++ b/astrbot/core/computer/booters/boxlite.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import asyncio +import functools import random -from typing import Any +from typing import TYPE_CHECKING, Any import aiohttp import boxlite @@ -10,6 +13,9 @@ from shipyard.shell import ShellComponent as ShipyardShellComponent from astrbot.api import logger +if TYPE_CHECKING: + from astrbot.core.agent.tool import FunctionTool + from ..olayer import FileSystemComponent, PythonComponent, ShellComponent from .base import ComputerBooter @@ -188,3 +194,24 @@ class BoxliteBooter(ComputerBooter): async def upload_file(self, path: str, file_name: str) -> dict: """Upload file to sandbox""" return await self.mocked.upload_file(path, file_name) + + @classmethod + @functools.cache + def _default_tools(cls) -> tuple[FunctionTool, ...]: + from astrbot.core.computer.tools import ( + ExecuteShellTool, + FileDownloadTool, + FileUploadTool, + PythonTool, + ) + + return ( + ExecuteShellTool(), + PythonTool(), + FileUploadTool(), + FileDownloadTool(), + ) + + @classmethod + def get_default_tools(cls) -> list[FunctionTool]: + return list(cls._default_tools()) diff --git a/astrbot/core/computer/booters/constants.py b/astrbot/core/computer/booters/constants.py new file mode 100644 index 000000000..f81e90c4f --- /dev/null +++ b/astrbot/core/computer/booters/constants.py @@ -0,0 +1,3 @@ +BOOTER_SHIPYARD = "shipyard" +BOOTER_SHIPYARD_NEO = "shipyard_neo" +BOOTER_BOXLITE = "boxlite" diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index 6379d1e48..75de9c438 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -1,12 +1,41 @@ +from __future__ import annotations + +import functools +from typing import TYPE_CHECKING + from shipyard import ShipyardClient, Spec from astrbot.api import logger +if TYPE_CHECKING: + from astrbot.core.agent.tool import FunctionTool + from ..olayer import FileSystemComponent, PythonComponent, ShellComponent from .base import ComputerBooter class ShipyardBooter(ComputerBooter): + @classmethod + @functools.cache + def _default_tools(cls) -> tuple[FunctionTool, ...]: + from astrbot.core.computer.tools import ( + ExecuteShellTool, + FileDownloadTool, + FileUploadTool, + PythonTool, + ) + + return ( + ExecuteShellTool(), + PythonTool(), + FileUploadTool(), + FileDownloadTool(), + ) + + @classmethod + def get_default_tools(cls) -> list[FunctionTool]: + return list(cls._default_tools()) + def __init__( self, endpoint_url: str, diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index 6304696ad..5048eed39 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -1,11 +1,15 @@ from __future__ import annotations +import functools import os import shlex -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from astrbot.api import logger +if TYPE_CHECKING: + from astrbot.core.agent.tool import FunctionTool + from ..olayer import ( BrowserComponent, FileSystemComponent, @@ -511,3 +515,80 @@ class ShipyardNeoBooter(ComputerBooter): except Exception as e: logger.error(f"Error checking Shipyard Neo sandbox availability: {e}") return False + + # ── Tool / prompt self-description ──────────────────────────── + + @classmethod + @functools.cache + def _base_tools(cls) -> tuple[FunctionTool, ...]: + """4 base + 11 Neo lifecycle = 15 tools (all Neo profiles).""" + from astrbot.core.computer.tools import ( + AnnotateExecutionTool, + CreateSkillCandidateTool, + CreateSkillPayloadTool, + EvaluateSkillCandidateTool, + ExecuteShellTool, + FileDownloadTool, + FileUploadTool, + GetExecutionHistoryTool, + GetSkillPayloadTool, + ListSkillCandidatesTool, + ListSkillReleasesTool, + PromoteSkillCandidateTool, + PythonTool, + RollbackSkillReleaseTool, + SyncSkillReleaseTool, + ) + + return ( + ExecuteShellTool(), + PythonTool(), + FileUploadTool(), + FileDownloadTool(), + GetExecutionHistoryTool(), + AnnotateExecutionTool(), + CreateSkillPayloadTool(), + GetSkillPayloadTool(), + CreateSkillCandidateTool(), + ListSkillCandidatesTool(), + EvaluateSkillCandidateTool(), + PromoteSkillCandidateTool(), + ListSkillReleasesTool(), + RollbackSkillReleaseTool(), + SyncSkillReleaseTool(), + ) + + @classmethod + @functools.cache + def _browser_tools(cls) -> tuple[FunctionTool, ...]: + from astrbot.core.computer.tools import ( + BrowserBatchExecTool, + BrowserExecTool, + RunBrowserSkillTool, + ) + + return (BrowserExecTool(), BrowserBatchExecTool(), RunBrowserSkillTool()) + + @classmethod + def get_default_tools(cls) -> list[FunctionTool]: + """Pre-boot: conservative full list (including browser).""" + return list(cls._base_tools()) + list(cls._browser_tools()) + + def get_tools(self) -> list[FunctionTool]: + """Post-boot: capability-filtered list.""" + caps = self.capabilities + if caps is None: + return self.__class__.get_default_tools() + tools = list(self._base_tools()) + if "browser" in caps: + tools.extend(self._browser_tools()) + return tools + + @classmethod + def get_system_prompt_parts(cls) -> list[str]: + from astrbot.core.computer.prompts import ( + NEO_FILE_PATH_PROMPT, + NEO_SKILL_LIFECYCLE_PROMPT, + ) + + return [NEO_FILE_PATH_PROMPT, NEO_SKILL_LIFECYCLE_PROMPT] diff --git a/astrbot/core/computer/computer_client.py b/astrbot/core/computer/computer_client.py index aa10d125e..efcdc8370 100644 --- a/astrbot/core/computer/computer_client.py +++ b/astrbot/core/computer/computer_client.py @@ -1,8 +1,11 @@ +from __future__ import annotations + import json import os import shutil import uuid from pathlib import Path +from typing import TYPE_CHECKING from astrbot.api import logger from astrbot.core.skills.skill_manager import SANDBOX_SKILLS_ROOT, SkillManager @@ -13,8 +16,12 @@ from astrbot.core.utils.astrbot_path import ( ) from .booters.base import ComputerBooter +from .booters.constants import BOOTER_BOXLITE, BOOTER_SHIPYARD, BOOTER_SHIPYARD_NEO from .booters.local import LocalBooter +if TYPE_CHECKING: + from astrbot.core.agent.tool import FunctionTool + session_booter: dict[str, ComputerBooter] = {} local_booter: ComputerBooter | None = None _MANAGED_SKILLS_FILE = ".astrbot_managed_skills.json" @@ -511,3 +518,56 @@ def get_local_booter() -> ComputerBooter: if local_booter is None: local_booter = LocalBooter() return local_booter + + +# --------------------------------------------------------------------------- +# Unified query API — used by ComputerToolProvider and subagent tool exec +# --------------------------------------------------------------------------- + + +def _get_booter_class(booter_type: str) -> type[ComputerBooter] | None: + """Map booter_type string to class (lazy import).""" + if booter_type == BOOTER_SHIPYARD: + from .booters.shipyard import ShipyardBooter + + return ShipyardBooter + elif booter_type == BOOTER_SHIPYARD_NEO: + from .booters.shipyard_neo import ShipyardNeoBooter + + return ShipyardNeoBooter + elif booter_type == BOOTER_BOXLITE: + from .booters.boxlite import BoxliteBooter + + return BoxliteBooter + logger.warning("[Computer] booter_class_lookup booter=%s found=false", booter_type) + return None + + +def get_sandbox_tools(session_id: str) -> list[FunctionTool]: + """Return precise tool list from a booted session, or [] if not booted.""" + booter = session_booter.get(session_id) + if booter is None: + return [] + return booter.get_tools() + + +def get_sandbox_capabilities(session_id: str) -> tuple[str, ...] | None: + """Return capability tuple from a booted session, or None if unavailable.""" + booter = session_booter.get(session_id) + if booter is None: + return None + return getattr(booter, "capabilities", None) + + +def get_default_sandbox_tools(sandbox_cfg: dict) -> list[FunctionTool]: + """Return conservative (pre-boot) tool list based on config. No instance needed.""" + booter_type = sandbox_cfg.get("booter", BOOTER_SHIPYARD_NEO) + cls = _get_booter_class(booter_type) + return cls.get_default_tools() if cls else [] + + +def get_sandbox_prompt_parts(sandbox_cfg: dict) -> list[str]: + """Return booter-specific system prompt fragments based on config.""" + booter_type = sandbox_cfg.get("booter", BOOTER_SHIPYARD_NEO) + cls = _get_booter_class(booter_type) + return cls.get_system_prompt_parts() if cls else [] diff --git a/astrbot/core/computer/computer_tool_provider.py b/astrbot/core/computer/computer_tool_provider.py index d292dbef8..998cb11f8 100644 --- a/astrbot/core/computer/computer_tool_provider.py +++ b/astrbot/core/computer/computer_tool_provider.py @@ -4,47 +4,31 @@ Encapsulates all sandbox / local tool injection logic previously hardcoded in ``astr_main_agent.py``. The main agent now calls ``provider.get_tools(ctx)`` / ``provider.get_system_prompt_addon(ctx)`` without knowing about specific tool classes. + +Tool lists are delegated to booter subclasses via ``get_default_tools()`` +and ``get_tools()`` (see ``booters/base.py``), so adding a new booter type +does not require changes here. """ from __future__ import annotations import os import platform +from functools import cache +from typing import TYPE_CHECKING from astrbot.api import logger -from astrbot.core.agent.tool import FunctionTool from astrbot.core.tool_provider import ToolProviderContext +if TYPE_CHECKING: + from astrbot.core.agent.tool import FunctionTool + # --------------------------------------------------------------------------- -# Lazy tool singletons — created once on first use, cached at module level. -# This mirrors the previous behaviour in astr_main_agent_resources.py -# but keeps everything co-located with the provider. +# Lazy local-mode tool cache # --------------------------------------------------------------------------- -_SANDBOX_TOOLS_CACHE: list[FunctionTool] | None = None _LOCAL_TOOLS_CACHE: list[FunctionTool] | None = None -_NEO_TOOLS_CACHE: list[FunctionTool] | None = None -_BROWSER_TOOLS_CACHE: list[FunctionTool] | None = None - - -def _get_sandbox_base_tools() -> list[FunctionTool]: - global _SANDBOX_TOOLS_CACHE - if _SANDBOX_TOOLS_CACHE is None: - from astrbot.core.computer.tools import ( - ExecuteShellTool, - FileDownloadTool, - FileUploadTool, - PythonTool, - ) - - _SANDBOX_TOOLS_CACHE = [ - ExecuteShellTool(), - PythonTool(), - FileUploadTool(), - FileDownloadTool(), - ] - return list(_SANDBOX_TOOLS_CACHE) def _get_local_tools() -> list[FunctionTool]: @@ -59,58 +43,8 @@ def _get_local_tools() -> list[FunctionTool]: return list(_LOCAL_TOOLS_CACHE) -def _get_neo_skill_tools() -> list[FunctionTool]: - global _NEO_TOOLS_CACHE - if _NEO_TOOLS_CACHE is None: - from astrbot.core.computer.tools import ( - AnnotateExecutionTool, - CreateSkillCandidateTool, - CreateSkillPayloadTool, - EvaluateSkillCandidateTool, - GetExecutionHistoryTool, - GetSkillPayloadTool, - ListSkillCandidatesTool, - ListSkillReleasesTool, - PromoteSkillCandidateTool, - RollbackSkillReleaseTool, - SyncSkillReleaseTool, - ) - - _NEO_TOOLS_CACHE = [ - GetExecutionHistoryTool(), - AnnotateExecutionTool(), - CreateSkillPayloadTool(), - GetSkillPayloadTool(), - CreateSkillCandidateTool(), - ListSkillCandidatesTool(), - EvaluateSkillCandidateTool(), - PromoteSkillCandidateTool(), - ListSkillReleasesTool(), - RollbackSkillReleaseTool(), - SyncSkillReleaseTool(), - ] - return list(_NEO_TOOLS_CACHE) - - -def _get_browser_tools() -> list[FunctionTool]: - global _BROWSER_TOOLS_CACHE - if _BROWSER_TOOLS_CACHE is None: - from astrbot.core.computer.tools import ( - BrowserBatchExecTool, - BrowserExecTool, - RunBrowserSkillTool, - ) - - _BROWSER_TOOLS_CACHE = [ - BrowserExecTool(), - BrowserBatchExecTool(), - RunBrowserSkillTool(), - ] - return list(_BROWSER_TOOLS_CACHE) - - # --------------------------------------------------------------------------- -# System-prompt constants (moved from astr_main_agent_resources.py) +# System-prompt helpers # --------------------------------------------------------------------------- SANDBOX_MODE_PROMPT = ( @@ -118,25 +52,6 @@ SANDBOX_MODE_PROMPT = ( "shell commands and Python code securely." ) -_NEO_PATH_RULE_PROMPT = ( - "\n[Shipyard Neo File Path Rule]\n" - "When using sandbox filesystem tools (upload/download/read/write/list/delete), " - "always pass paths relative to the sandbox workspace root. " - "Example: use `baidu_homepage.png` instead of `/workspace/baidu_homepage.png`.\n" -) - -_NEO_SKILL_LIFECYCLE_PROMPT = ( - "\n[Neo Skill Lifecycle Workflow]\n" - "When user asks to create/update a reusable skill in Neo mode, use lifecycle tools instead of directly writing local skill folders.\n" - "Preferred sequence:\n" - "1) Use `astrbot_create_skill_payload` to store canonical payload content and get `payload_ref`.\n" - "2) Use `astrbot_create_skill_candidate` with `skill_key` + `source_execution_ids` (and optional `payload_ref`) to create a candidate.\n" - "3) Use `astrbot_promote_skill_candidate` to release: `stage=canary` for trial; `stage=stable` for production.\n" - "For stable release, set `sync_to_local=true` to sync `payload.skill_markdown` into local `SKILL.md`.\n" - "Do not treat ad-hoc generated files as reusable Neo skills unless they are captured via payload/candidate/release.\n" - "To update an existing skill, create a new payload/candidate and promote a new release version; avoid patching old local folders directly.\n" -) - def _build_local_mode_prompt() -> str: system_name = platform.system() or "Unknown" @@ -159,7 +74,13 @@ def _build_local_mode_prompt() -> str: class ComputerToolProvider: - """Provides computer-use tools (local / sandbox) based on session context.""" + """Provides computer-use tools (local / sandbox) based on session context. + + Sandbox tool lists are delegated to booter subclasses so that each booter + declares its own capabilities. ``get_tools`` prefers the precise + post-boot tool list from a running session; when the sandbox has not yet + been booted it falls back to the conservative pre-boot default. + """ @staticmethod def get_all_tools() -> list[FunctionTool]: @@ -258,7 +179,18 @@ class ComputerToolProvider: # -- sandbox helpers ---------------------------------------------------- def _sandbox_tools(self, ctx: ToolProviderContext) -> list[FunctionTool]: - """Collect tools for sandbox mode.""" + """Collect tools for sandbox mode. + + Prefers the precise post-boot tool list from the running session's + booter instance (``get_sandbox_tools``). When the sandbox has not + yet been booted, falls back to the conservative pre-boot default + declared by the booter class (``get_default_sandbox_tools``). + """ + from astrbot.core.computer.computer_client import ( + get_default_sandbox_tools, + get_sandbox_tools, + ) + booter_type = ctx.sandbox_cfg.get("booter", "shipyard_neo") # Validate shipyard (non-neo) config @@ -271,42 +203,22 @@ class ComputerToolProvider: os.environ["SHIPYARD_ENDPOINT"] = ep os.environ["SHIPYARD_ACCESS_TOKEN"] = at - tools = _get_sandbox_base_tools() + # Prefer precise post-boot tools from the running session + booted_tools = get_sandbox_tools(ctx.session_id) + if booted_tools: + return booted_tools - if booter_type == "shipyard_neo": - sandbox_capabilities = self._get_sandbox_capabilities(ctx.session_id) - - # Browser tools if capability present (or unknown) - if sandbox_capabilities is None or "browser" in sandbox_capabilities: - tools.extend(_get_browser_tools()) - - # Neo skill lifecycle tools - tools.extend(_get_neo_skill_tools()) - - return tools + # Pre-boot: conservative default from booter class + return get_default_sandbox_tools(ctx.sandbox_cfg) def _sandbox_prompt_addon(self, ctx: ToolProviderContext) -> str: """Build system-prompt addon for sandbox mode.""" - parts: list[str] = [] - - booter_type = ctx.sandbox_cfg.get("booter", "shipyard_neo") - if booter_type == "shipyard_neo": - parts.append(_NEO_PATH_RULE_PROMPT) - parts.append(_NEO_SKILL_LIFECYCLE_PROMPT) + from astrbot.core.computer.computer_client import get_sandbox_prompt_parts + parts = get_sandbox_prompt_parts(ctx.sandbox_cfg) parts.append(f"\n{SANDBOX_MODE_PROMPT}\n") return "".join(parts) - @staticmethod - def _get_sandbox_capabilities(session_id: str) -> tuple[str, ...] | None: - """Query capabilities for an already-booted sandbox session.""" - from astrbot.core.computer.computer_client import session_booter - - existing_booter = session_booter.get(session_id) - if existing_booter is not None: - return getattr(existing_booter, "capabilities", None) - return None - def get_all_tools() -> list[FunctionTool]: """Module-level entry point for ``FunctionToolManager.register_internal_tools()``. diff --git a/astrbot/core/computer/prompts.py b/astrbot/core/computer/prompts.py new file mode 100644 index 000000000..fe85b544f --- /dev/null +++ b/astrbot/core/computer/prompts.py @@ -0,0 +1,24 @@ +"""Booter-specific system prompt fragments. + +Kept separate from ``tools/prompts.py`` (which holds agent-level prompts) +so that booter subclasses can import without pulling in unrelated constants. +""" + +NEO_FILE_PATH_PROMPT = ( + "\n[Shipyard Neo File Path Rule]\n" + "When using sandbox filesystem tools (upload/download/read/write/list/delete), " + "always pass paths relative to the sandbox workspace root. " + "Example: use `baidu_homepage.png` instead of `/workspace/baidu_homepage.png`.\n" +) + +NEO_SKILL_LIFECYCLE_PROMPT = ( + "\n[Neo Skill Lifecycle Workflow]\n" + "When user asks to create/update a reusable skill in Neo mode, use lifecycle tools instead of directly writing local skill folders.\n" + "Preferred sequence:\n" + "1) Use `astrbot_create_skill_payload` to store canonical payload content and get `payload_ref`.\n" + "2) Use `astrbot_create_skill_candidate` with `skill_key` + `source_execution_ids` (and optional `payload_ref`) to create a candidate.\n" + "3) Use `astrbot_promote_skill_candidate` to release: `stage=canary` for trial; `stage=stable` for production.\n" + "For stable release, set `sync_to_local=true` to sync `payload.skill_markdown` into local `SKILL.md`.\n" + "Do not treat ad-hoc generated files as reusable Neo skills unless they are captured via payload/candidate/release.\n" + "To update an existing skill, create a new payload/candidate and promote a new release version; avoid patching old local folders directly.\n" +) diff --git a/astrbot/core/tools/prompts.py b/astrbot/core/tools/prompts.py index e858f744a..f6816c172 100644 --- a/astrbot/core/tools/prompts.py +++ b/astrbot/core/tools/prompts.py @@ -132,7 +132,7 @@ FILE_EXTRACT_CONTEXT_TEMPLATE = ( ) CONVERSATION_HISTORY_INJECT_PREFIX = ( - "\n\nBellow is you and user previous conversation history:\n" + "\n\nBelow is your and the user's previous conversation history:\n" ) BACKGROUND_TASK_WOKE_USER_PROMPT = ( diff --git a/astrbot/core/tools/send_message.py b/astrbot/core/tools/send_message.py index fc285abd6..f7dd4fc9d 100644 --- a/astrbot/core/tools/send_message.py +++ b/astrbot/core/tools/send_message.py @@ -89,7 +89,9 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): context.context.event.unified_msg_origin, ) # Use shell to check if the file exists in sandbox - result = await sb.shell.exec(f"test -f {path} && echo '_&exists_'") + import shlex + + result = await sb.shell.exec(f"test -f {shlex.quote(path)} && echo '_&exists_'") if "_&exists_" in json.dumps(result): # Download the file from sandbox name = os.path.basename(path)