#!/usr/bin/env python3 """ config-generator.py — 环境变量驱动的 openclaw.json 生成器 三层配置合并: 1. 基础模板 (安全默认值) 2. 从备份恢复的 openclaw.json 3. 环境变量覆盖 (最高优先级) 端口: 18889 (内部端口,由 Caddy 反向代理) """ import json, os, copy STATE_DIR = os.environ.get("OPENCLAW_STATE_DIR", "/root/.openclaw") GATEWAY_PORT = int(os.environ.get("OPENCLAW_GATEWAY_PORT", 18889)) def deep_merge(base: dict, overlay: dict) -> dict: result = copy.deepcopy(base) for k, v in overlay.items(): if k in result and isinstance(result[k], dict) and isinstance(v, dict): result[k] = deep_merge(result[k], v) else: result[k] = copy.deepcopy(v) return result def _get_gw_password() -> str: pw = os.environ.get("OPENCLAW_GATEWAY_PASSWORD", "") or os.environ.get("OPENCLAW_GATEWAY_TOKEN", "") if not pw: import secrets pw = "openclaw-" + secrets.token_hex(8) print(f"[config] Generated gateway password: {pw}") return pw def generate() -> dict: # ── Layer 1: 基础模板 ── config = { "models": {"providers": {}}, "agents": {"defaults": {}, "list": []}, "skills": {"entries": {}, "allowBundled": []}, "plugins": {"entries": {}, "allow": []}, "gateway": { "mode": "local", "bind": "lan", "port": GATEWAY_PORT, "trustedProxies": ["0.0.0.0/0"], "auth": { "mode": "token", "token": _get_gw_password(), }, "controlUi": { "enabled": True, "allowInsecureAuth": True, "dangerouslyDisableDeviceAuth": True, "dangerouslyAllowHostHeaderOriginFallback": True, }, }, "browser": { "enabled": False, "headless": True, }, } # ── Layer 2: 合并恢复的配置 ── restored_path = os.path.join(STATE_DIR, "openclaw.json") if os.path.exists(restored_path): try: with open(restored_path) as f: restored = json.load(f) config = deep_merge(config, restored) except Exception as e: print(f"[config] Warning: could not load restored config: {e}") # ── Layer 3: 环境变量覆盖 ── # 3a. 模型提供商 (只在有 API base + key 时生成) provider = os.environ.get("LLM_PROVIDER", "default") base_url = os.environ.get("OPENAI_API_BASE", "").rstrip("/") base_url = base_url.replace("/chat/completions", "") if base_url and os.environ.get("OPENAI_API_KEY"): config["models"]["providers"][provider] = { "baseUrl": base_url, "apiKey": os.environ.get("OPENAI_API_KEY", ""), "api": "openai-completions", "models": [ { "id": os.environ.get("MODEL", "gpt-4o"), "name": os.environ.get("MODEL", "gpt-4o"), "contextWindow": 128000, } ], } # 额外模型 (JSON array) extra = os.environ.get("EXTRA_MODELS", "") if extra: try: config["models"]["providers"][provider]["models"].extend(json.loads(extra)) except Exception: pass else: # 无 API 配置时占位,避免配置验证失败 config["models"]["providers"]["placeholder"] = { "baseUrl": "http://localhost:8080/v1", "apiKey": "not-configured-yet", "api": "openai-completions", "models": [{"id": "placeholder", "name": "placeholder", "contextWindow": 4096}], } print("[config] Warning: OPENAI_API_BASE or OPENAI_API_KEY not set, using placeholder") # 3b. 兼容旧版 plugins.allow → plugins.bundledDiscovery config["plugins"]["bundledDiscovery"] = "compat" # 3b. Agent 默认配置 default_model = f"{provider}/{os.environ.get('MODEL', 'gpt-4o')}" config["agents"]["defaults"] = { "model": {"primary": default_model}, "workspace": os.path.join(STATE_DIR, "workspace"), } # 3c. Agents: 5 role-based + 1 coordinator default_names = ["开发组-产品经理", "开发组-架构师", "开发组-开发", "开发组-测试", "开发组-项目经理", "总协调"] for i in range(1, 7): # 1-5 role agents, 6 coordinator if i == 6: agent_id = "coordinator" name = os.environ.get("COORDINATOR_NAME", default_names[5]) workspace = os.path.join(STATE_DIR, "workspace-coordinator") model_override = default_model else: agent_id = f"agent-{i}" name = os.environ.get(f"AGENT_{i}_NAME", default_names[i - 1]) workspace = os.path.join(STATE_DIR, f"workspace-agent-{i}") model_override = os.environ.get(f"AGENT_{i}_MODEL", default_model) agent_def = { "id": agent_id, "name": name, "workspace": workspace, "model": {"primary": model_override}, } # Skills skills_str = os.environ.get(f"AGENT_{i}_SKILLS", "") if skills_str: agent_def["skills"] = [s.strip() for s in skills_str.split(",")] config["agents"]["list"].append(agent_def) # 3d. Skills skills_enabled = os.environ.get("SKILLS_ENABLED", "") if skills_enabled: for s in skills_enabled.split(","): s = s.strip() if s: config["skills"]["entries"][s] = {"enabled": True} # 3e. Plugins plugins_allow = os.environ.get("PLUGINS_ALLOW", "") if plugins_allow: config["plugins"]["allow"] = [p.strip() for p in plugins_allow.split(",")] else: # Default plugin set (only bundled skills, not external plugins) config["plugins"]["allow"] = [ "multi-search-cn", "github", "summarize", ] # Enable bundled skills (these are in plugins.entries as skills) core_skills = ["multi-search-cn", "github", "summarize"] for p in core_skills: if p not in config["plugins"]["allow"]: config["plugins"]["allow"].append(p) config["plugins"]["entries"][p] = {"enabled": True} return config if __name__ == "__main__": config = generate() os.makedirs(STATE_DIR, exist_ok=True) out = os.path.join(STATE_DIR, "openclaw.json") with open(out, "w") as f: json.dump(config, f, indent=2, ensure_ascii=False) # Debug: output full config to logs print("[config] Full configuration (JSON):\n" + json.dumps(config, indent=2, ensure_ascii=False)) print(f"[config] Generated → {out}")