Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion astrbot/core/astr_main_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,14 +494,26 @@ async def _ensure_persona_and_skills(
skill_manager = SkillManager()
skills = skill_manager.list_skills(active_only=True, runtime=runtime)
skills = _filter_skills_for_current_config(skills, cfg)
workspace_skills = (
skill_manager.list_workspace_skills(
_get_workspace_path_for_umo(event.unified_msg_origin)
)
if runtime == "local"
else []
)

if skills:
if skills or workspace_skills:
if persona and persona.get("skills") is not None:
if not persona["skills"]:
skills = []
else:
allowed = set(persona["skills"])
skills = [skill for skill in skills if skill.name in allowed]
if workspace_skills and (not persona or persona.get("skills") != []):
skills_by_name = {skill.name: skill for skill in skills}
for skill in workspace_skills:
skills_by_name[skill.name] = skill
skills = [skills_by_name[name] for name in sorted(skills_by_name)]
if skills:
req.system_prompt += f"\n{build_skills_prompt(skills)}\n"
if runtime == "none":
Expand Down
81 changes: 80 additions & 1 deletion astrbot/core/skills/skill_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
DEFAULT_SKILLS_CONFIG: dict[str, dict] = {"skills": {}}
SANDBOX_SKILLS_ROOT = "skills"
SANDBOX_WORKSPACE_ROOT = "/workspace"
WORKSPACE_SKILLS_ROOT = "skills"
WORKSPACE_SKILL_FRONTMATTER_MAX_CHARS = 64 * 1024
_SANDBOX_SKILLS_CACHE_VERSION = 1

_SKILL_NAME_RE = re.compile(r"^[\w.-]+$")
Expand Down Expand Up @@ -216,7 +218,7 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str:
display_name = _sanitize_skill_display_name(skill.name)

description = skill.description or "No description"
if skill.source_type == "sandbox_only":
if skill.source_type in {"sandbox_only", "workspace"}:
description = _sanitize_prompt_description(description)
if not description:
description = "Read SKILL.md for details."
Expand Down Expand Up @@ -337,6 +339,83 @@ def _get_plugin_skill_dir(self, name: str) -> Path | None:
return skill_dir
return None

def list_workspace_skills(
self, workspace_root: str | Path | None
) -> list[SkillInfo]:
"""List request-scoped skills from a session workspace.

Args:
workspace_root: The current session workspace directory.

Returns:
Skills discovered under ``<workspace_root>/skills``.
"""
if not workspace_root:
return []

raw_workspace_root = Path(workspace_root)
skills_root = raw_workspace_root / WORKSPACE_SKILLS_ROOT
if not skills_root.is_dir():
return []

try:
resolved_workspace_root = raw_workspace_root.resolve(strict=True)
resolved_skills_root = skills_root.resolve(strict=True)
if not resolved_skills_root.is_relative_to(resolved_workspace_root):
return []
skill_dirs = sorted(
resolved_skills_root.iterdir(), key=lambda item: item.name
)
except OSError:
return []

skills: list[SkillInfo] = []
for skill_dir in skill_dirs:
if not skill_dir.is_dir():
continue
skill_name = skill_dir.name
if not _SKILL_NAME_RE.match(skill_name):
continue
try:
entry_names = {entry.name for entry in skill_dir.iterdir()}
except OSError:
continue
if "SKILL.md" not in entry_names:
continue
skill_md = skill_dir / "SKILL.md"
if not skill_md.is_file():
continue

try:
resolved_skill_md = skill_md.resolve(strict=True)
except OSError:
continue
if not resolved_skill_md.is_relative_to(resolved_skills_root):
continue

description = ""
try:
with resolved_skill_md.open(encoding="utf-8") as f:
content = f.read(WORKSPACE_SKILL_FRONTMATTER_MAX_CHARS)
description = _parse_frontmatter_description(content)
except (OSError, UnicodeError):
description = ""

skills.append(
SkillInfo(
name=skill_name,
description=description,
path=resolved_skill_md.as_posix(),
active=True,
source_type="workspace",
source_label="workspace",
local_exists=True,
readonly=True,
)
)

return skills

def _load_config(self) -> dict:
if not os.path.exists(self.config_path):
self._save_config(DEFAULT_SKILLS_CONFIG.copy())
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/i18n/locales/en-US/features/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@
"neoPayloadTitle": "Neo Payload",
"neoPayloadFailed": "Failed to load payload",
"runtimeNoneWarning": "Computer Use runtime is set to None; Skills may not run correctly because no runtime is enabled.",
"runtimeHint": "Set the Computer Use runtime to Local or Sandbox in settings so AstrBot can use your Skills.",
"runtimeHint": "Set the Computer Use runtime to Local or Sandbox in settings so AstrBot can use your Skills. Workspace Skills are not shown on this page yet.",
"neoRuntimeRequired": "Neo Skills are available only when runtime is sandbox and sandbox booter is shipyard_neo.",
"sourceLocalOnly": "Local Skill",
"sourceSandboxOnly": "Sandbox Preset Skill",
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/i18n/locales/ru-RU/features/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@
"neoPayloadTitle": "Детали Neo Payload",
"neoPayloadFailed": "Ошибка чтения Payload",
"runtimeNoneWarning": "Среда выполнения Computer Use не задана. Навыки могут не работать, так как нет активного окружения.",
"runtimeHint": "Установите среду выполнения в «local» или «sandbox» в настройках способностей использования компьютера.",
"runtimeHint": "Установите среду выполнения в «local» или «sandbox» в настройках способностей использования компьютера. Навыки из рабочей области пока не отображаются на этой странице.",
"neoRuntimeRequired": "Neo Skills доступны только в среде sandbox с драйвером shipyard_neo.",
"sourceLocalOnly": "Локальный навык",
"sourceSandboxOnly": "Предустановленный Sandbox навык",
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/i18n/locales/zh-CN/features/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@
"neoPayloadTitle": "Neo Payload 详情",
"neoPayloadFailed": "读取 Payload 失败",
"runtimeNoneWarning": "Computer Use 运行环境为无,Skills 可能无法正确被 Agent 运行,因为没有启用运行环境。",
"runtimeHint": "需要在配置的 “使用电脑能力” 中将运行环境设置为 “local” 或 “sandbox” 才能让 AstrBot 正常使用你提供的 Skills。",
"runtimeHint": "需要在配置的 “使用电脑能力” 中将运行环境设置为 “local” 或 “sandbox” 才能让 AstrBot 正常使用你提供的 Skills。工作区的 Skills 暂不在此页面显示。",
"neoRuntimeRequired": "Neo Skills 仅在运行环境为 sandbox 且沙箱驱动为 shipyard_neo 时可用。",
"sourceLocalOnly": "本地 Skill",
"sourceSandboxOnly": "Sandbox 预置 Skill",
Expand Down
28 changes: 26 additions & 2 deletions docs/en/use/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,32 @@ Open the AstrBot admin panel, navigate to the `Plugins` page, and find `Skills`.
You can upload Skills with the following requirements:

1. The upload must be a `.zip` archive.
2. **After extraction, it must contain a single Skill folder. The folder name will be used as the identifier for the Skill in AstrBot—please name it using English characters.**
3. The Skill folder must include a file named `SKILL.md`, and its contents should preferably follow the Anthropic Skills specification. You can refer to Anthropic's documentation: https://code.claude.com/docs/en/skills
2. After extraction, it can contain one or more Skill folders. Each folder name is used as the Skill identifier in AstrBot. Use English letters, numbers, dots, underscores, or hyphens.
3. Each Skill folder must include a file named exactly `SKILL.md`. The filename is case-sensitive. Its contents should preferably follow the Anthropic Skills specification. You can refer to Anthropic's documentation: https://code.claude.com/docs/en/skills

## Skill Sources and Priority

AstrBot can discover Skills from several places:

- **Local Skills**: uploaded from the WebUI or placed under `data/skills/<skill_name>/SKILL.md`. These appear in the WebUI Skills management page.
- **Plugin-provided Skills**: plugins can bundle Skills in their own `skills/` directory. They appear in the WebUI, but are managed by the plugin, so they cannot be deleted or edited from the Local Skills page.
- **Sandbox preset Skills**: when the sandbox runtime is used, AstrBot reads Skills discovered inside the sandbox and provides them to the Agent.
- **Workspace Skills**: Skills under the current session workspace, at `skills/<skill_name>/SKILL.md`. They are currently injected only in local runtime, where the path is usually `data/workspaces/{normalized_umo}/skills/<skill_name>/SKILL.md`.

Workspace Skills are **request-scoped**. In local runtime, when AstrBot builds a request, it checks the current session workspace for a `skills/` directory and appends valid Skills to that request's Skill inventory. They are not shown in the WebUI Skills management page yet, and they are not written to the global Skills configuration.

If a persona is configured to select specific Skills, that list filters only local, plugin-provided, and sandbox Skills. Workspace Skills are still discovered and injected as part of the current request. Workspace Skills are disabled only when the persona is explicitly configured to use no Skills.

When multiple sources contain a Skill with the same name, request-time priority is:

1. If the current persona is explicitly configured to use no Skills, no Skills are injected, including Workspace Skills.
2. If the current persona selects a specific Skill list, that list does not filter Workspace Skills.
3. The current session's Workspace Skill has the highest priority. If it has the same name as a local, plugin, or sandbox Skill, it overrides that Skill for the current request only.
4. Local Skills take priority over plugin-provided Skills and sandbox-only Skills.
5. Plugin-provided Skills take priority over sandbox-only Skills.
6. Sandbox-only Skills are injected only when there is no local, plugin, or workspace Skill with the same name.

If a local Skill has been synced into the sandbox, AstrBot treats it as the same Skill. In sandbox runtime, the request will prefer the path that is readable inside the sandbox. Workspace Skills are not automatically synced into the sandbox yet.

## Using Skills in AstrBot

Expand Down
29 changes: 26 additions & 3 deletions docs/zh/use/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,32 @@ AstrBot 在 v4.13.0 之后引入了对 Anthropic Skills 的支持,使得用户
你可以上传 Skills,上传格式要求如下:

1. 是一个 .zip 压缩包
2. **解压后是一个 Skill 文件夹,Skill 文件夹的名字即为这个 Skill 在 AstrBot 中的标识,请用英文命名**。
3. Skill 文件夹内必须包含一个名为 `SKILL.md` 的文件,且该文件内容最好符合 Anthropic Skills 规范。你可以参考 [Anthropic 技能](https://code.claude.com/docs/zh-CN/skills)
2. 解压后可以是一个或多个 Skill 文件夹,Skill 文件夹的名字即为这个 Skill 在 AstrBot 中的标识,请用英文、数字、点、下划线或短横线命名。
3. Skill 文件夹内必须包含一个名为 `SKILL.md` 的文件,且文件名大小写需要完全一致。该文件内容最好符合 Anthropic Skills 规范。你可以参考 [Anthropic 技能](https://code.claude.com/docs/zh-CN/skills)

## Skill 来源与优先级

AstrBot 会从多个位置发现 Skills:

- **本地 Skills**:通过 WebUI 上传或放置在 `data/skills/<skill_name>/SKILL.md`,会显示在 WebUI 的 Skills 管理页面中。
- **插件内置 Skills**:插件可以在自己的 `skills/` 目录中提供 Skills。它们会显示在 WebUI 中,但由插件管理,因此不能在本地 Skills 页面删除或编辑。
- **Sandbox 预置 Skills**:使用 sandbox 运行环境时,AstrBot 会读取沙盒中已发现的 Skills,并在请求时提供给 Agent。
- **工作区 Skills**:当前会话 workspace 下的 `skills/<skill_name>/SKILL.md`。目前仅在 local 运行环境下注入,路径通常是 `data/workspaces/{normalized_umo}/skills/<skill_name>/SKILL.md`。

工作区 Skills 是**请求级**能力:local 运行环境下,AstrBot 会在每次构建请求时检测当前会话 workspace 下的 `skills/` 目录,并把合法的 Skills 拼进本次请求的 Skills 清单。它们暂时不会显示在 WebUI 的 Skills 管理页面,也不会写入全局 Skills 配置。

如果人格配置为“选择指定 Skills”,该列表只用于筛选本地、插件内置和 sandbox Skills;工作区 Skills 仍会作为当前请求的一部分被检测并注入。只有人格明确配置为“不使用任何 Skills”时,才会同时禁用工作区 Skills。

当不同来源出现同名 Skill 时,请求中的优先级如下:

1. 如果当前人格明确配置为“不使用任何 Skills”,则不会注入任何 Skills,包括工作区 Skills。
2. 如果当前人格配置了指定 Skills 列表,该列表不会过滤工作区 Skills。
3. 当前会话的工作区 Skill 优先级最高。同名时,它会覆盖本地、插件或 sandbox 中的同名 Skill,仅对当前请求生效。
4. 本地 Skills 优先于插件内置 Skills 和 sandbox-only Skills。
5. 插件内置 Skills 优先于 sandbox-only Skills。
6. sandbox-only Skills 只会在没有同名本地、插件或工作区 Skill 时作为可用 Skill 注入。

如果本地 Skill 已同步到 sandbox,AstrBot 会把它视为同一个 Skill;在 sandbox 运行环境下,请求中会优先使用 sandbox 内可读取的路径。工作区 Skills 暂不会自动同步到 sandbox。

## 在 AstrBot 使用 Skills

Expand All @@ -35,4 +59,3 @@ Skills 提供了 Agent 操作说明书,并且内容通常包含 Python 代码

> [!NOTE]
> 需要说明的是,如果您使用 Local 作为执行环境,AstrBot 目前仅允许 **AstrBot 管理员**请求时才真正让 Agent 操作你的本地环境,普通用户将会被禁止,Agent 将无法通过 Shell、Python 等 Tool 在本地环境执行代码,会收到相应的权限限制提示,如 `Sorry, I cannot execute code on your local environment due to permission restrictions.`。

Loading
Loading