mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 12:24:46 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
11
.env.example
Normal file
11
.env.example
Normal file
@@ -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
|
||||||
@@ -43,6 +43,9 @@ class AioSandboxProvider(SandboxProvider):
|
|||||||
- host_path: /path/on/host
|
- host_path: /path/on/host
|
||||||
container_path: /path/in/container
|
container_path: /path/in/container
|
||||||
read_only: false
|
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):
|
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,
|
"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,
|
"container_prefix": sandbox_config.container_prefix or DEFAULT_CONTAINER_PREFIX,
|
||||||
"mounts": sandbox_config.mounts or [],
|
"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:
|
def _is_sandbox_ready(self, base_url: str, timeout: int = 30) -> bool:
|
||||||
"""Check if sandbox is ready to accept connections.
|
"""Check if sandbox is ready to accept connections.
|
||||||
|
|
||||||
@@ -191,6 +215,10 @@ class AioSandboxProvider(SandboxProvider):
|
|||||||
container_name,
|
container_name,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Add configured environment variables
|
||||||
|
for key, value in self._config["environment"].items():
|
||||||
|
cmd.extend(["-e", f"{key}={value}"])
|
||||||
|
|
||||||
# Add configured volume mounts
|
# Add configured volume mounts
|
||||||
for mount in self._config["mounts"]:
|
for mount in self._config["mounts"]:
|
||||||
host_path = mount.host_path
|
host_path = mount.host_path
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class SandboxConfig(BaseModel):
|
|||||||
auto_start: Whether to automatically start Docker container (default: true)
|
auto_start: Whether to automatically start Docker container (default: true)
|
||||||
container_prefix: Prefix for container names (default: deer-flow-sandbox)
|
container_prefix: Prefix for container names (default: deer-flow-sandbox)
|
||||||
mounts: List of volume mounts to share directories with the container
|
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(
|
use: str = Field(
|
||||||
@@ -52,5 +53,10 @@ class SandboxConfig(BaseModel):
|
|||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="List of volume mounts to share directories between host and container",
|
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")
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|||||||
@@ -147,6 +147,14 @@ sandbox:
|
|||||||
# # - host_path: /path/on/host
|
# # - host_path: /path/on/host
|
||||||
# # container_path: /home/user/shared
|
# # container_path: /home/user/shared
|
||||||
# # read_only: false
|
# # 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
|
# Skills Configuration
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { env } from "@/env";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
|
|
||||||
import type { MCPConfig } from "./types";
|
import type { MCPConfig } from "./types";
|
||||||
|
|
||||||
export async function loadMCPConfig() {
|
export async function loadMCPConfig() {
|
||||||
const response = await fetch(
|
const response = await fetch(`${getBackendBaseURL()}/api/mcp/config`);
|
||||||
`${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/mcp/config`,
|
|
||||||
);
|
|
||||||
return response.json() as Promise<MCPConfig>;
|
return response.json() as Promise<MCPConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMCPConfig(config: MCPConfig) {
|
export async function updateMCPConfig(config: MCPConfig) {
|
||||||
const response = await fetch(
|
const response = await fetch(`${getBackendBaseURL()}/api/mcp/config`,
|
||||||
`${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/mcp/config`,
|
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { env } from "@/env";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
|
|
||||||
import type { Skill } from "./type";
|
import type { Skill } from "./type";
|
||||||
|
|
||||||
export async function loadSkills() {
|
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();
|
const json = await skills.json();
|
||||||
return json.skills as Skill[];
|
return json.skills as Skill[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enableSkill(skillName: string, enabled: boolean) {
|
export async function enableSkill(skillName: string, enabled: boolean) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/skills/${skillName}`,
|
`${getBackendBaseURL()}/api/skills/${skillName}`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -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)
|
1. Extract story context (theme, gadget, conflict, punchline)
|
||||||
2. Map to 8 narrative beats
|
2. Map to 8 narrative beats
|
||||||
3. Output JSON to `/mnt/user-data/outputs/prompt.json`
|
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
|
5. Directly present the output image as well as the `prompt.json` using the `present_files` tool without checking the file existence
|
||||||
|
|
||||||
## Panel Layout
|
## Panel Layout
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
def generate_image(prompt: str) -> str:
|
def generate_image(prompt: str, output_path: str) -> str:
|
||||||
api_key = os.getenv("GEMINI_API_KEY")
|
api_key = os.getenv("GEMINI_API_KEY")
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return "GEMINI_API_KEY is not set"
|
return "GEMINI_API_KEY is not set"
|
||||||
@@ -24,22 +25,25 @@ def generate_image(prompt: str) -> str:
|
|||||||
if len(image_parts) == 1:
|
if len(image_parts) == 1:
|
||||||
base64_image = image_parts[0]["inlineData"]["data"]
|
base64_image = image_parts[0]["inlineData"]["data"]
|
||||||
# Save the image to a file
|
# 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))
|
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:
|
else:
|
||||||
return "Failed to generate image"
|
return "Failed to generate image"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(input_path: str, output_path: str):
|
||||||
with open(
|
with open(
|
||||||
"/mnt/user-data/outputs/prompt.json",
|
input_path,
|
||||||
"r",
|
"r",
|
||||||
) as f:
|
) as f:
|
||||||
raw = f.read()
|
raw = f.read()
|
||||||
print(generate_image(raw))
|
print(generate_image(raw, output_path))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
parser = argparse.ArgumentParser(description="Generate Doraemon comic image")
|
||||||
main()
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user