feat(skills): include uploads directory in skill scanning

This commit is contained in:
Titan 2026-04-02 17:37:24 +08:00
parent 66bdc951f8
commit 5aa38ee108
5 changed files with 82 additions and 12 deletions

View File

@ -394,7 +394,17 @@ def get_skills_prompt_section(available_skills: set[str] | None = None) -> str:
Returns the <skill_system>...</skill_system> block listing all enabled skills,
suitable for injection into any agent's system prompt.
"""
skills = _get_enabled_skills()
thread_id = None
try:
from langgraph.config import get_config
config = get_config()
thread_id = config.get("configurable", {}).get("thread_id")
except Exception:
pass
# Keep regular enable/disable behavior while letting uploads be default-enabled in loader.
skills = load_skills(enabled_only=True, thread_id=thread_id)
try:
from deerflow.config import get_app_config
@ -561,4 +571,6 @@ def apply_prompt_template(subagent_enabled: bool = False, max_concurrent_subagen
acp_section=acp_and_mounts_section,
)
logger.debug("Generated full system prompt:\n%s", prompt)
return prompt + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>"

View File

@ -192,8 +192,8 @@ class ExtensionsConfig(BaseModel):
"""
skill_config = self.skills.get(skill_name)
if skill_config is None:
# Default to enable for public & custom skill
return skill_category in ("public", "custom")
# Default to enable for public/custom/uploads skills.
return skill_category in ("public", "custom", "uploads")
return skill_config.enabled

View File

@ -8,6 +8,27 @@ from .types import Skill
logger = logging.getLogger(__name__)
UPLOADS_SKILLS_PATH = Path("/mnt/user-data/uploads")
def get_uploads_skills_path(thread_id: str | None = None) -> Path:
"""Resolve the uploads skills root for the current execution context.
When called from the LangGraph process, uploaded skills live under the
host-side per-thread data directory rather than the sandbox mount path.
"""
if not thread_id:
return UPLOADS_SKILLS_PATH
try:
from deerflow.config.paths import get_paths
return get_paths().sandbox_uploads_dir(thread_id)
except Exception as exc:
logger.warning("Failed to resolve uploads skills path for thread %s: %s", thread_id, exc)
return UPLOADS_SKILLS_PATH
def get_skills_root_path() -> Path:
"""
Get the root path of the skills directory.
@ -22,12 +43,19 @@ def get_skills_root_path() -> Path:
return skills_dir
def load_skills(skills_path: Path | None = None, use_config: bool = True, enabled_only: bool = False) -> list[Skill]:
def load_skills(
skills_path: Path | None = None,
use_config: bool = True,
enabled_only: bool = False,
thread_id: str | None = None,
) -> list[Skill]:
"""
Load all skills from the skills directory.
Scans both public and custom skill directories, parsing SKILL.md files
to extract metadata. The enabled state is determined by the skills_state_config.json file.
Scans public/custom skill directories under the configured skills root,
and also scans uploaded skills under /mnt/user-data/uploads.
SKILL.md metadata is parsed and enabled state is derived from
skills_state_config.json.
Args:
skills_path: Optional custom path to skills directory.
@ -35,6 +63,8 @@ def load_skills(skills_path: Path | None = None, use_config: bool = True, enable
Otherwise defaults to deer-flow/skills
use_config: Whether to load skills path from config (default: True)
enabled_only: If True, only return enabled skills (default: False)
thread_id: Optional thread ID used to resolve per-thread uploads skills
from the LangGraph host process
Returns:
List of Skill objects, sorted by name
@ -57,12 +87,22 @@ def load_skills(skills_path: Path | None = None, use_config: bool = True, enable
skills = []
# Scan public and custom directories
for category in ["public", "custom"]:
category_path = skills_path / category
# Scan built-in roots and uploaded skills mounted in personal workspace.
scan_targets: list[tuple[str, Path]] = [
("public", skills_path / "public"),
("custom", skills_path / "custom"),
("uploads", get_uploads_skills_path(thread_id)),
]
for category, category_path in scan_targets:
logger.debug("Scanning %s skills under %s", category, category_path)
if not category_path.exists() or not category_path.is_dir():
logger.debug("Skip %s scan: directory not found or not a directory (%s)", category, category_path)
continue
scanned_skill_dirs: list[str] = []
for current_root, dir_names, file_names in os.walk(category_path, followlinks=True):
# Keep traversal deterministic and skip hidden directories.
dir_names[:] = sorted(name for name in dir_names if not name.startswith("."))
@ -71,11 +111,22 @@ def load_skills(skills_path: Path | None = None, use_config: bool = True, enable
skill_file = Path(current_root) / "SKILL.md"
relative_path = skill_file.parent.relative_to(category_path)
scanned_skill_dirs.append(relative_path.as_posix())
skill = parse_skill_file(skill_file, category=category, relative_path=relative_path)
if skill:
skills.append(skill)
if scanned_skill_dirs:
logger.debug(
"%s scan found %d skill directories: %s",
category,
len(scanned_skill_dirs),
", ".join(sorted(scanned_skill_dirs)),
)
else:
logger.debug("%s scan found no skill directories", category)
# Load skills state configuration and update enabled status
# NOTE: We use ExtensionsConfig.from_file() instead of get_extensions_config()
# to always read the latest configuration from disk. This ensures that changes
@ -86,6 +137,10 @@ def load_skills(skills_path: Path | None = None, use_config: bool = True, enable
extensions_config = ExtensionsConfig.from_file()
for skill in skills:
if skill.category == "uploads":
# Uploaded skills should be available by default for the current thread.
skill.enabled = True
continue
skill.enabled = extensions_config.is_skill_enabled(skill.name, skill.category)
except Exception as e:
# If config loading fails, default to all enabled

View File

@ -13,7 +13,7 @@ def parse_skill_file(skill_file: Path, category: str, relative_path: Path | None
Args:
skill_file: Path to the SKILL.md file
category: Category of the skill ('public' or 'custom')
category: Category of the skill ('public', 'custom', or 'uploads')
Returns:
Skill object if parsing succeeds, None otherwise

View File

@ -12,7 +12,7 @@ class Skill:
skill_dir: Path
skill_file: Path
relative_path: Path # Relative path from category root to skill directory
category: str # 'public' or 'custom'
category: str # 'public', 'custom', or 'uploads'
enabled: bool = False # Whether this skill is enabled
@property
@ -31,7 +31,10 @@ class Skill:
Returns:
Full container path to the skill directory
"""
category_base = f"{container_base_path}/{self.category}"
if self.category == "uploads":
category_base = "/mnt/user-data/uploads"
else:
category_base = f"{container_base_path}/{self.category}"
skill_path = self.skill_path
if skill_path:
return f"{category_base}/{skill_path}"