diff --git a/routes/bot_manager.py b/routes/bot_manager.py index 66229a6..aefa1b5 100644 --- a/routes/bot_manager.py +++ b/routes/bot_manager.py @@ -10,6 +10,7 @@ import secrets import os import shutil from datetime import datetime, timedelta +from pathlib import Path from typing import Optional, List from fastapi import APIRouter, HTTPException, Header from pydantic import BaseModel @@ -1803,6 +1804,23 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header( elif not dataset_ids: dataset_ids = None + # 扫描 skills 需要的环境变量并合并到 shell_env + shell_env = settings.get('shell_env') or {} + skills_list = settings.get('skills') + if skills_list: + from utils.multi_project_manager import scan_skill_env_keys + # skills 可能是逗号分隔的字符串,需要拆分成列表 + if isinstance(skills_list, str): + skills_names = [s.strip() for s in skills_list.split(',') if s.strip()] + else: + skills_names = skills_list + required_keys = scan_skill_env_keys(bot_uuid, skills_names, Path("projects")) + for key in required_keys: + if key not in shell_env: + shell_env[key] = '' + # 清理不在 skill 所需变量中且值为空的环境变量 + shell_env = {k: v for k, v in shell_env.items() if v or k in required_keys} + return BotSettingsResponse( bot_id=str(bot_id), name=bot_name, @@ -1818,8 +1836,8 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header( enable_memori=settings.get('enable_memori', False), enable_thinking=settings.get('enable_thinking', False), tool_response=settings.get('tool_response', False), - skills=settings.get('skills'), - shell_env=settings.get('shell_env'), + skills=skills_list, + shell_env=shell_env, is_published=is_published if is_published else False, is_owner=is_owner, copied_from=str(copied_from) if copied_from else None, diff --git a/utils/multi_project_manager.py b/utils/multi_project_manager.py index 885f7d3..100d443 100644 --- a/utils/multi_project_manager.py +++ b/utils/multi_project_manager.py @@ -4,6 +4,7 @@ """ import os +import re import shutil import json import logging @@ -426,3 +427,64 @@ def _extract_skills_to_robot(bot_id: str, skills: List[str], project_path: Path) logger.info(f" Copied: {source_dir} -> {target_dir}") except Exception as e: logger.error(f" Failed to copy {source_dir}: {e}") + + +_COMMON_ENV_KEYS = frozenset({ + 'TMPDIR', 'PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'TERM', + 'PWD', 'OLDPWD', 'NODE_ENV', +}) + +_ENV_PATTERNS = [ + re.compile(r'process\.env\.(\w+)'), + re.compile(r'os\.getenv\([\'"](\w+)'), + re.compile(r'os\.environ\.get\([\'"](\w+)'), + re.compile(r'os\.environ\[[\'"](\w+)'), +] + +_SCAN_EXTENSIONS = {'.js', '.py', '.ts', '.sh', '.md', '.jsx', '.tsx', '.mjs', '.cjs'} + + +def scan_skill_env_keys(bot_id: str, skills: List[str], project_path: Path) -> set: + """ + 扫描 skills 目录下所有文件,提取引用的环境变量 KEY。 + + Args: + bot_id: 机器人 ID + skills: 技能名称列表 + project_path: 项目路径(如 Path("projects")) + + Returns: + set: 环境变量 KEY 集合(排除通用变量) + """ + skills_source_dirs = [ + project_path / "uploads" / bot_id / "skills", + Path("skills"), + ] + + env_keys = set() + + for skill in skills: + source_dir = None + for base_dir in skills_source_dirs: + candidate = base_dir / skill + if candidate.exists(): + source_dir = candidate + break + + if source_dir is None or not source_dir.exists(): + continue + + for file_path in source_dir.rglob('*'): + if not file_path.is_file(): + continue + if file_path.suffix.lower() not in _SCAN_EXTENSIONS: + continue + try: + content = file_path.read_text(encoding='utf-8', errors='ignore') + except Exception: + continue + for pattern in _ENV_PATTERNS: + env_keys.update(pattern.findall(content)) + + env_keys -= _COMMON_ENV_KEYS + return env_keys