From 6e147a772e8fc61c341a606b9637c60c1d40ced3 Mon Sep 17 00:00:00 2001 From: hetao Date: Sat, 24 Jan 2026 22:33:29 +0800 Subject: [PATCH] feat: add environment variable injection for Docker sandbox - Add environment field to sandbox config for injecting env vars into container - Support $VAR syntax to resolve values from host environment variables - Refactor frontend API modules to use centralized getBackendBaseURL() - Improve Doraemon skill with explicit input/output path arguments - Add .env.example file Co-Authored-By: Claude Opus 4.5 --- .env.example | 11 ++++++++ .../aio_sandbox/aio_sandbox_provider.py | 28 +++++++++++++++++++ backend/src/config/sandbox_config.py | 6 ++++ config.example.yaml | 8 ++++++ frontend/src/core/mcp/api.ts | 9 ++---- frontend/src/core/skills/api.ts | 6 ++-- skills/public/doraemon-comic-aigc/SKILL.md | 2 +- .../doraemon-comic-aigc/scripts/generate.py | 20 +++++++------ 8 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ee90676 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# TAVILY API Key +TAVILY_API_KEY=your-tavily-api-key + +# Jina API Key +JINA_API_KEY=your-jina-api-key + +# Optional: +# VOLCENGINE_API_KEY=your-volcengine-api-key +# OPENAI_API_KEY=your-openai-api-key +# GEMINI_API_KEY=your-gemini-api-key +# DEEPSEEK_API_KEY=your-deepseek-api-key \ No newline at end of file diff --git a/backend/src/community/aio_sandbox/aio_sandbox_provider.py b/backend/src/community/aio_sandbox/aio_sandbox_provider.py index 7cd871d..7cb8c2f 100644 --- a/backend/src/community/aio_sandbox/aio_sandbox_provider.py +++ b/backend/src/community/aio_sandbox/aio_sandbox_provider.py @@ -43,6 +43,9 @@ class AioSandboxProvider(SandboxProvider): - host_path: /path/on/host container_path: /path/in/container read_only: false + environment: # Environment variables to inject (values starting with $ are resolved from host env) + NODE_ENV: production + API_KEY: $MY_API_KEY """ def __init__(self): @@ -94,8 +97,29 @@ class AioSandboxProvider(SandboxProvider): "auto_start": sandbox_config.auto_start if sandbox_config.auto_start is not None else True, "container_prefix": sandbox_config.container_prefix or DEFAULT_CONTAINER_PREFIX, "mounts": sandbox_config.mounts or [], + "environment": self._resolve_env_vars(sandbox_config.environment or {}), } + def _resolve_env_vars(self, env_config: dict[str, str]) -> dict[str, str]: + """Resolve environment variable references in configuration. + + Values starting with $ are resolved from host environment variables. + + Args: + env_config: Dictionary of environment variable names to values. + + Returns: + Dictionary with resolved environment variable values. + """ + resolved = {} + for key, value in env_config.items(): + if isinstance(value, str) and value.startswith("$"): + env_name = value[1:] # Remove $ prefix + resolved[key] = os.environ.get(env_name, "") + else: + resolved[key] = str(value) + return resolved + def _is_sandbox_ready(self, base_url: str, timeout: int = 30) -> bool: """Check if sandbox is ready to accept connections. @@ -191,6 +215,10 @@ class AioSandboxProvider(SandboxProvider): container_name, ] + # Add configured environment variables + for key, value in self._config["environment"].items(): + cmd.extend(["-e", f"{key}={value}"]) + # Add configured volume mounts for mount in self._config["mounts"]: host_path = mount.host_path diff --git a/backend/src/config/sandbox_config.py b/backend/src/config/sandbox_config.py index d4164c9..0447938 100644 --- a/backend/src/config/sandbox_config.py +++ b/backend/src/config/sandbox_config.py @@ -22,6 +22,7 @@ class SandboxConfig(BaseModel): auto_start: Whether to automatically start Docker container (default: true) container_prefix: Prefix for container names (default: deer-flow-sandbox) mounts: List of volume mounts to share directories with the container + environment: Environment variables to inject into the container (values starting with $ are resolved from host env) """ use: str = Field( @@ -52,5 +53,10 @@ class SandboxConfig(BaseModel): default_factory=list, description="List of volume mounts to share directories between host and container", ) + environment: dict[str, str] = Field( + default_factory=dict, + description="Environment variables to inject into the sandbox container. " + "Values starting with $ will be resolved from host environment variables.", + ) model_config = ConfigDict(extra="allow") diff --git a/config.example.yaml b/config.example.yaml index 0312b7a..b41dbc1 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -147,6 +147,14 @@ sandbox: # # - host_path: /path/on/host # # container_path: /home/user/shared # # read_only: false +# +# # Optional: Environment variables to inject into the sandbox container +# # Values starting with $ will be resolved from host environment variables +# # environment: +# # NODE_ENV: production +# # DEBUG: "false" +# # API_KEY: $MY_API_KEY # Reads from host's MY_API_KEY env var +# # DATABASE_URL: $DATABASE_URL # Reads from host's DATABASE_URL env var # ============================================================================ # Skills Configuration diff --git a/frontend/src/core/mcp/api.ts b/frontend/src/core/mcp/api.ts index edc4561..c63a3e8 100644 --- a/frontend/src/core/mcp/api.ts +++ b/frontend/src/core/mcp/api.ts @@ -1,17 +1,14 @@ -import { env } from "@/env"; +import { getBackendBaseURL } from "@/core/config"; import type { MCPConfig } from "./types"; export async function loadMCPConfig() { - const response = await fetch( - `${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/mcp/config`, - ); + const response = await fetch(`${getBackendBaseURL()}/api/mcp/config`); return response.json() as Promise; } export async function updateMCPConfig(config: MCPConfig) { - const response = await fetch( - `${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/mcp/config`, + const response = await fetch(`${getBackendBaseURL()}/api/mcp/config`, { method: "PUT", headers: { diff --git a/frontend/src/core/skills/api.ts b/frontend/src/core/skills/api.ts index 242d932..a558492 100644 --- a/frontend/src/core/skills/api.ts +++ b/frontend/src/core/skills/api.ts @@ -1,16 +1,16 @@ -import { env } from "@/env"; +import { getBackendBaseURL } from "@/core/config"; import type { Skill } from "./type"; export async function loadSkills() { - const skills = await fetch(`${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/skills`); + const skills = await fetch(`${getBackendBaseURL()}/api/skills`); const json = await skills.json(); return json.skills as Skill[]; } export async function enableSkill(skillName: string, enabled: boolean) { const response = await fetch( - `${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/skills/${skillName}`, + `${getBackendBaseURL()}/api/skills/${skillName}`, { method: "PUT", headers: { diff --git a/skills/public/doraemon-comic-aigc/SKILL.md b/skills/public/doraemon-comic-aigc/SKILL.md index 6fe77c5..5011e5e 100644 --- a/skills/public/doraemon-comic-aigc/SKILL.md +++ b/skills/public/doraemon-comic-aigc/SKILL.md @@ -12,7 +12,7 @@ Generate JSON spec for 8 panels arranged on ONE 9:16 vertical canvas (1080x1920) 1. Extract story context (theme, gadget, conflict, punchline) 2. Map to 8 narrative beats 3. Output JSON to `/mnt/user-data/outputs/prompt.json` -4. Run `python /mnt/skills/custom/doraemon-comic-aigc/scripts/generate.py` +4. Run `python /mnt/skills/custom/doraemon-comic-aigc/scripts/generate.py --input_path /mnt/user-data/outputs/prompt.json --output_path /mnt/user-data/outputs/doraemon.png ` 5. Directly present the output image as well as the `prompt.json` using the `present_files` tool without checking the file existence ## Panel Layout diff --git a/skills/public/doraemon-comic-aigc/scripts/generate.py b/skills/public/doraemon-comic-aigc/scripts/generate.py index 0f6933f..e5aedba 100644 --- a/skills/public/doraemon-comic-aigc/scripts/generate.py +++ b/skills/public/doraemon-comic-aigc/scripts/generate.py @@ -1,10 +1,11 @@ +import argparse import base64 import os import requests -def generate_image(prompt: str) -> str: +def generate_image(prompt: str, output_path: str) -> str: api_key = os.getenv("GEMINI_API_KEY") if not api_key: return "GEMINI_API_KEY is not set" @@ -24,22 +25,25 @@ def generate_image(prompt: str) -> str: if len(image_parts) == 1: base64_image = image_parts[0]["inlineData"]["data"] # Save the image to a file - with open("/mnt/user-data/outputs/doraemon.png", "wb") as f: + with open(output_path, "wb") as f: f.write(base64.b64decode(base64_image)) - return "Successfully generated image to /mnt/user-data/outputs/doraemon.png" + return f"Successfully generated image to {output_path}" else: return "Failed to generate image" -def main(): +def main(input_path: str, output_path: str): with open( - "/mnt/user-data/outputs/prompt.json", + input_path, "r", ) as f: raw = f.read() - print(generate_image(raw)) + print(generate_image(raw, output_path)) if __name__ == "__main__": - main() - main() + parser = argparse.ArgumentParser(description="Generate Doraemon comic image") + parser.add_argument("--input_path", required=True, help="Path to the input prompt JSON file") + parser.add_argument("--output_path", required=True, help="Path to save the output image") + args = parser.parse_args() + main(args.input_path, args.output_path)